Skip to content

Commit

Permalink
feat(new-hope): add react hook form support in combobox
Browse files Browse the repository at this point in the history
§
  • Loading branch information
iljs committed Nov 12, 2024
1 parent 989263c commit a110e90
Show file tree
Hide file tree
Showing 16 changed files with 2,664 additions and 72 deletions.
1 change: 1 addition & 0 deletions packages/plasma-b2c/api/plasma-b2c.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { CellTextboxLabel } from '@salutejs/plasma-new-hope/styled-components';
import { CellTextboxSubtitle } from '@salutejs/plasma-new-hope/styled-components';
import { CellTextboxTitle } from '@salutejs/plasma-new-hope/styled-components';
import { ChangeEvent } from 'react';
import { ChangeEventHandler } from 'react';
import { ChangeInstanceCallback } from '@salutejs/plasma-new-hope/types/components/DatePicker/RangeDate/RangeDate.types';
import { CheckboxProps as CheckboxProps_2 } from '@salutejs/plasma-new-hope/types/components/Checkbox/Checkbox.types';
import { ChipGroupProps } from '@salutejs/plasma-new-hope/types/components/ChipGroup/ChipGroup.types';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import React, { forwardRef, useState, useReducer, useMemo, createContext, useLayoutEffect, useRef } from 'react';
import type { ChangeEvent } from 'react';
import React, {
forwardRef,
useState,
useReducer,
useMemo,
createContext,
useLayoutEffect,
useRef,
useEffect,
} from 'react';
import type { ChangeEvent, ForwardedRef } from 'react';
import { safeUseId, useForkRef } from '@salutejs/plasma-core';

import { RootProps } from '../../../engines';
Expand Down Expand Up @@ -27,18 +36,22 @@ import type { ItemContext, ComboboxProps } from './Combobox.types';
import { base as viewCSS } from './variations/_view/base';
import { base as sizeCSS } from './variations/_size/base';
import type { ItemOptionTransformed } from './ui/Inner/ui/Item/Item.types';
import { Form } from './ui/Form/Form';

export const Context = createContext<ItemContext>({} as ItemContext);

/**
* Поле ввода с выпадающим списком и возможностью фильтрации и выбора элементов.
*/

export const comboboxRoot = (Root: RootProps<HTMLInputElement, Omit<ComboboxProps, 'items'>>) =>
forwardRef<HTMLInputElement, ComboboxProps>((props, ref) => {
forwardRef<HTMLInputElement | HTMLSelectElement, ComboboxProps>((props, ref) => {
const {
name,
multiple,
value: outerValue,
onChange: outerOnChange,
defaultValue,
isTargetAmount,
targetAmount,
items,
Expand Down Expand Up @@ -126,12 +139,24 @@ export const comboboxRoot = (Root: RootProps<HTMLInputElement, Omit<ComboboxProp
}
}, floatingPopoverRef);

const onChange = (newValue: string | Array<string>) => {
const onChange = (newValue: string | Array<string> | ChangeEvent<HTMLSelectElement> | null) => {
if (outerOnChange) {
outerOnChange(newValue as any);
if (!name && !multiple && typeof newValue === 'string') {
outerOnChange(newValue as string & string[] & ChangeEvent<HTMLSelectElement>);
}

if (!name && multiple && Array.isArray(newValue)) {
outerOnChange(newValue as string & string[] & ChangeEvent<HTMLSelectElement>);
}

if (name && typeof newValue === 'object' && !Array.isArray(newValue)) {
outerOnChange(newValue as string & string[] & ChangeEvent<HTMLSelectElement>);
}
}

setInternalValue(newValue);
if (typeof newValue === 'string' || Array.isArray(newValue)) {
setInternalValue(newValue);
}
};

const handleClickArrow = () => {
Expand Down Expand Up @@ -317,8 +342,23 @@ export const comboboxRoot = (Root: RootProps<HTMLInputElement, Omit<ComboboxProp
// А переменную, содержащую сложные типы данных, нельзя помещать в deps.
}, [outerValue, internalValue, items]);

useEffect(() => {
if (defaultValue) {
setInternalValue(defaultValue);
}
}, [defaultValue]);

return (
<Root size={size} view={view} labelPlacement={labelPlacement} disabled={disabled} readOnly={readOnly}>
{name && (
<Form
name={name}
value={internalValue}
multiple={multiple}
onChange={onChange}
ref={ref as ForwardedRef<HTMLSelectElement>}
/>
)}
<div>
<Context.Provider
value={{
Expand All @@ -343,7 +383,7 @@ export const comboboxRoot = (Root: RootProps<HTMLInputElement, Omit<ComboboxProp
listWidth={listWidth}
target={(referenceRef) => (
<StyledTextField
ref={inputForkRef}
ref={name ? inputRef : (inputForkRef as ForwardedRef<HTMLInputElement>)}
inputWrapperRef={referenceRef}
value={textValue}
onChange={handleTextValueChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CSSProperties, ButtonHTMLAttributes } from 'react';
import type { CSSProperties, ButtonHTMLAttributes, ChangeEventHandler } from 'react';
import React from 'react';

import { RequiredProps } from '../../TextField/TextField.types';
Expand All @@ -23,8 +23,10 @@ type Placement =

type IsMultiselect<T extends ItemOption = ItemOption> =
| {
name?: never;
multiple?: false;
value?: string;
defaultValue?: never;
onChange?: (value: string) => void;
/**
* Если включено - будет выведено общее количество выбранных элементов вместо перечисления.
Expand All @@ -41,13 +43,35 @@ type IsMultiselect<T extends ItemOption = ItemOption> =
| {
multiple: true;
value?: Array<string>;
defaultValue?: never;
onChange?: (value: Array<string>) => void;
isTargetAmount?: true;
targetAmount?: number;
/**
* Callback для кастомной настройки значения в селекте.
*/
renderValue?: (item: T) => string;
name?: never;
}
| {
multiple: true;
value?: string[];
defaultValue?: string[];
onChange?: ChangeEventHandler;
isTargetAmount?: never | false;
targetAmount?: never;
renderValue?: never;
name: string;
}
| {
multiple?: false;
value?: string;
defaultValue?: string;
onChange?: ChangeEventHandler;
isTargetAmount?: never | false;
targetAmount?: never;
renderValue?: never;
name: string;
};

type ViewStateProps =
Expand Down Expand Up @@ -180,7 +204,7 @@ export type ComboboxProps<T extends ItemOption = ItemOption> = {
} & ViewStateProps &
IsMultiselect<T> &
RequiredProps &
Omit<ButtonHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'>;
Omit<ButtonHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'name' | 'defaultValue'>;

export type FloatingPopoverProps = {
target: React.ReactNode | ((ref: React.MutableRefObject<HTMLElement | null>) => React.ReactNode);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { styled } from '@linaria/react';

import { applyHidden } from '../../../../../mixins';

export const SelectHidden = styled.select`
${applyHidden()};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { ChangeEvent, forwardRef, useEffect, useRef } from 'react';
import { useForkRef } from '@salutejs/plasma-core';

import { createEvent } from '../../../../../utils/syntheticEvent';
import { ComboboxProps } from '../../Combobox.types';

import { SelectHidden } from './Form.styles';

type Props = Pick<ComboboxProps, 'name' | 'value' | 'multiple'> & {
onChange: (value: ChangeEvent<HTMLSelectElement> | null) => void;
};

export const Form = forwardRef<HTMLSelectElement, Props>(({ name, multiple, value, onChange }, ref) => {
const values = (multiple ? value : [value]) as string[];
const selectRef = useRef<HTMLSelectElement | null>(null);
const forkRef = useForkRef(selectRef, ref);

useEffect(() => {
const event = createEvent(selectRef);
onChange && onChange(event);
}, [values]);
return (
<>
<SelectHidden ref={forkRef} multiple={multiple} name={name} hidden value={multiple ? values : values[0]}>
{values.map((v) => (
<option key={v} value={v}>
{v}
</option>
))}
</SelectHidden>
</>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,11 @@ const items = [

const SingleStory = (args: StorySelectProps) => {
const [value, setValue] = useState('');

return (
<div style={{ width: '400px' }}>
<Combobox
{...args}
name="mau"
items={items}
value={value}
onChange={setValue}
Expand Down
44 changes: 44 additions & 0 deletions packages/plasma-new-hope/src/utils/syntheticEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { RefObject } from 'react';

export const createEvent = <T extends HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement>(
ref: RefObject<T>,
) => {
if (ref.current) {
const event = new Event('change', { bubbles: true });
Object.defineProperty(event, 'target', { writable: false, value: ref.current });
const syntheticEvent = createSyntheticEvent(event) as React.ChangeEvent<typeof ref.current>;
return syntheticEvent;
}

return null;
};

export const createSyntheticEvent = <T extends Element, E extends Event>(event: E): React.SyntheticEvent<T, E> => {
let isDefaultPrevented = false;
let isPropagationStopped = false;
const preventDefault = () => {
isDefaultPrevented = true;
event.preventDefault();
};
const stopPropagation = () => {
isPropagationStopped = true;
event.stopPropagation();
};
return {
nativeEvent: event,
currentTarget: event.currentTarget as EventTarget & T,
target: event.target as EventTarget & T,
bubbles: event.bubbles,
cancelable: event.cancelable,
defaultPrevented: event.defaultPrevented,
eventPhase: event.eventPhase,
isTrusted: event.isTrusted,
preventDefault,
isDefaultPrevented: () => isDefaultPrevented,
stopPropagation,
isPropagationStopped: () => isPropagationStopped,
persist: () => {},
timeStamp: event.timeStamp,
type: event.type,
};
};
1 change: 1 addition & 0 deletions packages/plasma-web/api/plasma-web.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { CellTextboxLabel } from '@salutejs/plasma-new-hope/styled-components';
import { CellTextboxSubtitle } from '@salutejs/plasma-new-hope/styled-components';
import { CellTextboxTitle } from '@salutejs/plasma-new-hope/styled-components';
import { ChangeEvent } from 'react';
import { ChangeEventHandler } from 'react';
import { ChangeInstanceCallback } from '@salutejs/plasma-new-hope/types/components/DatePicker/RangeDate/RangeDate.types';
import { CheckboxProps as CheckboxProps_2 } from '@salutejs/plasma-new-hope/types/components/Checkbox/Checkbox.types';
import { ChipGroupProps } from '@salutejs/plasma-new-hope/types/components/ChipGroup/ChipGroup.types';
Expand Down
Loading

0 comments on commit a110e90

Please sign in to comment.