From 96360f2e1b54b1131fd3099ec835d5744e8669dd Mon Sep 17 00:00:00 2001 From: Kostiantyn Huzenko Date: Thu, 14 Nov 2024 20:04:47 -0500 Subject: [PATCH] feat(select): L3-3818 updated select --- src/components/Select/Select.stories.tsx | 43 ++++++++++++++++++++++++ src/components/Select/Select.test.tsx | 40 ++++++++++++++++++++++ src/components/Select/Select.tsx | 34 ++++++++++++++++--- src/components/Select/select.scss | 42 +++++++++++++++++++++++ src/components/Select/types.ts | 4 +++ src/index.ts | 1 + 6 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 src/components/Select/select.scss create mode 100644 src/components/Select/types.ts diff --git a/src/components/Select/Select.stories.tsx b/src/components/Select/Select.stories.tsx index 02139589..8e2dbfe4 100644 --- a/src/components/Select/Select.stories.tsx +++ b/src/components/Select/Select.stories.tsx @@ -1,6 +1,7 @@ import type { Meta } from '@storybook/react'; import Select, { SelectProps } from './Select'; +import { SelectVariants } from './types'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta = { title: 'Components/Select', @@ -24,6 +25,11 @@ const argTypes = { type: 'boolean', }, }, + showCustomIcon: { + control: { + type: 'boolean', + }, + }, invalid: { control: { type: 'boolean', @@ -54,6 +60,12 @@ const argTypes = { type: 'select', }, }, + variant: { + options: [SelectVariants.default, SelectVariants.tertiary], + control: { + type: 'select', + }, + }, value: { control: { type: 'text', @@ -90,14 +102,45 @@ Playground.args = { playgroundWidth: 300, className: 'input-test-class', defaultValue: 'Option 2', + showCustomIcon: false, disabled: false, invalid: false, invalidText: 'Error message', labelText: 'Label text', readOnly: false, size: 'md', + variant: SelectVariants.default, warn: false, warnText: 'Warning message that is really long can wrap to more lines.', }; Playground.argTypes = argTypes; + +export const Tertiary = ({ playgroundWidth, ...args }: StoryProps) => ( +
+ +
+); + +Tertiary.args = { + showCustomIcon: true, + variant: SelectVariants.tertiary, +}; + +Tertiary.argTypes = { + variant: { + options: [SelectVariants.default, SelectVariants.tertiary], + control: { + type: 'select', + }, + defaultValue: SelectVariants.tertiary, + }, +}; diff --git a/src/components/Select/Select.test.tsx b/src/components/Select/Select.test.tsx index dba36ab6..e4a31965 100644 --- a/src/components/Select/Select.test.tsx +++ b/src/components/Select/Select.test.tsx @@ -3,9 +3,17 @@ import userEvent from '@testing-library/user-event'; import * as React from 'react'; import Select from './Select'; +import { px } from '../../utils'; describe('A Select', () => { const reqProps = { labelText: 'My Test Label', id: 'test-id' }; + const mockLabel = 'Test Label'; + const mockOptions = ( + <> + + + + ); it('will render a default value if passed', () => { const testRef = React.createRef(); @@ -54,4 +62,36 @@ describe('A Select', () => { await userEvent.selectOptions(screen.getByTestId('test-id'), ['option one']); await waitFor(() => expect(mockedOnChange.mock.calls).toHaveLength(0)); }); + + it('should toggle --open and --closed classes on click', async () => { + render( + , + ); + + const selectElement = screen.getByTestId('test-select'); + + // Initial state should be closed + expect(selectElement).toHaveClass(`${px}-input__select--closed`); + + // Simulate click to open + await userEvent.click(selectElement); + expect(selectElement).toHaveClass(`${px}-input__select--open`); + + // Simulate blur to close + await userEvent.tab(); + expect(selectElement).toHaveClass(`${px}-input__select--closed`); + }); + + it('should apply --tertiary class when variant is tertiary', () => { + render( + , + ); + + const selectElement = screen.getByTestId('test-select-tertiary'); + expect(selectElement).toHaveClass(`${px}-input__select--tertiary`); + }); }); diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index 562da613..d59284e9 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -1,15 +1,23 @@ import * as React from 'react'; import classnames from 'classnames'; - import { px, useNormalizedInputProps } from '../../utils'; - import { InputProps } from '../Input/Input'; +import './select.scss'; +import { SelectVariants } from './types'; export interface SelectProps extends InputProps { /** * Option elements that are selectable */ children: React.ReactNode; + /** + * Determines if you want to show the icon + */ + showCustomIcon?: boolean; + /** + * Determines the variant of the select + */ + variant: SelectVariants; } /** @@ -31,6 +39,8 @@ const Select = React.forwardRef( disabled, hideLabel, id, + showCustomIcon = false, + variant = SelectVariants.default, inline, invalid, invalidText, @@ -48,6 +58,9 @@ const Select = React.forwardRef( ) => { const type = 'select'; const inputProps = useNormalizedInputProps({ disabled, id, invalid, invalidText, readOnly, type, warn, warnText }); + const [isOpen, setIsOpen] = React.useState(false); + const handleIsOpen = () => setIsOpen((prev) => !prev); + const closeDropdown = () => setIsOpen(false); const wrapperClassnames = classnames(`${px}-${type}-input`, `${px}-input`, `${px}-input--${size}`, { [`${px}-input--inline`]: inline, @@ -58,19 +71,32 @@ const Select = React.forwardRef( [`${className}__wrapper`]: className, }); + const selectClassnames = classnames(`${px}-input__input`, { + className, + [`${px}-input__select--open`]: isOpen && showCustomIcon, + [`${px}-input__select--closed`]: !isOpen && showCustomIcon, + [`${px}-input__select--tertiary`]: variant === SelectVariants.tertiary, + }); + + const handleClick = (e: React.MouseEvent) => { + handleIsOpen(); + onClick?.(e); + }; + return (