From 08d1dd0e567f50c5c9d75267440ebbd1fd48d893 Mon Sep 17 00:00:00 2001 From: Victoria Zhizhonkova Date: Fri, 19 Apr 2024 13:23:43 +0700 Subject: [PATCH] feat(FormItem): add required prop (#6820) * feat(FormItem): add required prop * CHORE: Update screenshots * fix: a11y issue with required indicator --------- Co-authored-by: GitHub Action --- .../FormItem/FormItem.e2e-playground.tsx | 5 ++ .../components/FormItem/FormItem.module.css | 5 ++ .../vkui/src/components/FormItem/FormItem.tsx | 52 ++++++++++++------- .../FormItem/FormItemTop/FormItemTopLabel.tsx | 13 ++++- .../vkui/src/components/FormItem/Readme.md | 2 + .../formitem-android-chromium-dark-1-snap.png | 4 +- ...formitem-android-chromium-light-1-snap.png | 4 +- .../formitem-ios-webkit-dark-1-snap.png | 4 +- .../formitem-ios-webkit-light-1-snap.png | 4 +- .../formitem-vkcom-chromium-dark-1-snap.png | 4 +- .../formitem-vkcom-chromium-light-1-snap.png | 4 +- .../formitem-vkcom-firefox-dark-1-snap.png | 4 +- .../formitem-vkcom-firefox-light-1-snap.png | 4 +- .../formitem-vkcom-webkit-dark-1-snap.png | 4 +- .../formitem-vkcom-webkit-light-1-snap.png | 4 +- .../vkui/src/components/FormItem/context.ts | 11 ++++ 16 files changed, 87 insertions(+), 41 deletions(-) create mode 100644 packages/vkui/src/components/FormItem/context.ts diff --git a/packages/vkui/src/components/FormItem/FormItem.e2e-playground.tsx b/packages/vkui/src/components/FormItem/FormItem.e2e-playground.tsx index 887ef29424..b2f020c7b0 100644 --- a/packages/vkui/src/components/FormItem/FormItem.e2e-playground.tsx +++ b/packages/vkui/src/components/FormItem/FormItem.e2e-playground.tsx @@ -63,6 +63,11 @@ export const FormItemPlayground = (props: ComponentPlaygroundProps) => { , ], }, + { + top: ['Сверху'], + children: [], + required: [true], + }, ]} > {(props: FormItemProps) => } diff --git a/packages/vkui/src/components/FormItem/FormItem.module.css b/packages/vkui/src/components/FormItem/FormItem.module.css index 831bdec674..c17b7236cd 100644 --- a/packages/vkui/src/components/FormItem/FormItem.module.css +++ b/packages/vkui/src/components/FormItem/FormItem.module.css @@ -52,6 +52,11 @@ white-space: normal; } +.FormItemTop__label--required { + color: var(--vkui--color_text_negative); + margin-inline-start: 3px; +} + .FormItem__bottom { color: var(--vkui--color_text_secondary); padding-block-start: 8px; diff --git a/packages/vkui/src/components/FormItem/FormItem.tsx b/packages/vkui/src/components/FormItem/FormItem.tsx index 2e79ba1c03..3dd80a6033 100644 --- a/packages/vkui/src/components/FormItem/FormItem.tsx +++ b/packages/vkui/src/components/FormItem/FormItem.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { classNames, hasReactNode, noop } from '@vkontakte/vkjs'; import { useAdaptivity } from '../../hooks/useAdaptivity'; import { useExternRef } from '../../hooks/useExternRef'; +import { useObjectMemo } from '../../hooks/useObjectMemo'; import { HasComponent, HasRootRef } from '../../types'; import { Removable, RemovableProps } from '../Removable/Removable'; import { RootComponent } from '../RootComponent/RootComponent'; @@ -9,6 +10,7 @@ import { Footnote } from '../Typography/Footnote/Footnote'; import { FormItemTop } from './FormItemTop/FormItemTop'; import { FormItemTopAside } from './FormItemTop/FormItemTopAside'; import { FormItemTopLabel } from './FormItemTop/FormItemTopLabel'; +import { FormItemContext } from './context'; import styles from './FormItem.module.css'; const sizeYClassNames = { @@ -64,6 +66,10 @@ export interface FormItemProps * @since 5.8.0 */ noPadding?: boolean; + /** + * Помечает поле обязательным + */ + required?: boolean; } /** @@ -84,6 +90,7 @@ export const FormItem = ({ bottomId, noPadding, topNode, + required = false, ...restProps }: FormItemProps) => { const rootEl = useExternRef(getRootRef); @@ -113,6 +120,8 @@ export const FormItem = ({ ); + const context = useObjectMemo({ required, topMultiline }); + return ( - {removable ? ( - { - if (rootEl?.current) { - onRemove(e, rootEl.current); - } - }} - removePlaceholder={removePlaceholder} - indent={removable === 'indent'} - > -
+ {removable ? ( + { + if (rootEl?.current) { + onRemove(e, rootEl.current); + } + }} + removePlaceholder={removePlaceholder} + indent={removable === 'indent'} > - {wrappedChildren} -
-
- ) : ( - wrappedChildren - )} +
+ {wrappedChildren} +
+ + ) : ( + wrappedChildren + )} +
); }; diff --git a/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopLabel.tsx b/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopLabel.tsx index 3f7ab02eb4..ce50c1ce9d 100644 --- a/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopLabel.tsx +++ b/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopLabel.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; import { HasComponent, HasRootRef } from '../../../types'; import { Subhead } from '../../Typography/Subhead/Subhead'; +import { FormItemContext } from '../context'; import styles from '../FormItem.module.css'; export interface FormItemTopLabelProps @@ -10,6 +11,7 @@ export interface FormItemTopLabelProps HasComponent { /** * Многострочный вывод заголовка. + * TODO [>=7]: удалить и вседа брать из контекста */ multiline?: boolean; } @@ -25,21 +27,28 @@ export const FormItemTopLabel = ({ children, Component: componentProp, htmlFor, - multiline = false, + multiline, ...restProps }: FormItemTopLabelProps) => { const component = componentProp || (htmlFor && 'label') || 'span'; + const { required, topMultiline: multilineContext } = React.useContext(FormItemContext); + return ( {children} + {required && ( + + * + + )} ); }; diff --git a/packages/vkui/src/components/FormItem/Readme.md b/packages/vkui/src/components/FormItem/Readme.md index 1ed3b86971..4212178522 100644 --- a/packages/vkui/src/components/FormItem/Readme.md +++ b/packages/vkui/src/components/FormItem/Readme.md @@ -103,6 +103,7 @@ const Example = () => { email ? 'Электронная почта введена верно!' : 'Пожалуйста, введите электронную почту' } bottomId="email-type" + required > { htmlFor="purpose-of-the-trip-select-id" bottom={purpose ? '' : 'Пожалуйста, укажите цель поездки'} status={purpose ? 'valid' : 'error'} + required >