From 33e1e38bed0251338d5580a1231851704c8f556f Mon Sep 17 00:00:00 2001 From: Egor Berezovskiy Date: Fri, 3 Jan 2025 15:57:28 +0100 Subject: [PATCH] front: ui-core input tooltip status messages Signed-off-by: Egor Berezovskiy --- .../src/components/inputs/FieldWrapper.tsx | 4 + ui-core/src/components/inputs/Input.tsx | 3 + .../src/components/inputs/InputStatusIcon.tsx | 2 +- .../src/components/inputs/StatusMessage.tsx | 23 +++++- ui-core/src/stories/Input.stories.tsx | 77 +++++++++++++++++- ui-core/src/styles/inputs/fieldWrapper.css | 14 +++- ui-core/src/styles/inputs/statusMessage.css | 81 +++++++++++++++++++ 7 files changed, 196 insertions(+), 8 deletions(-) diff --git a/ui-core/src/components/inputs/FieldWrapper.tsx b/ui-core/src/components/inputs/FieldWrapper.tsx index 51f3fa3d..5efe1199 100644 --- a/ui-core/src/components/inputs/FieldWrapper.tsx +++ b/ui-core/src/components/inputs/FieldWrapper.tsx @@ -18,6 +18,7 @@ export type FieldWrapperProps = { small?: boolean; children?: React.ReactNode; className?: string; + onCloseStatusMessage?: () => void; }; const FieldWrapper = ({ @@ -31,6 +32,7 @@ const FieldWrapper = ({ small = false, className, children, + onCloseStatusMessage, }: FieldWrapperProps) => { const statusClassname = statusWithMessage ? { [statusWithMessage.status]: true } : {}; @@ -65,8 +67,10 @@ const FieldWrapper = ({ {/* STATUS MESSAGE */} {statusWithMessage && ( )} diff --git a/ui-core/src/components/inputs/Input.tsx b/ui-core/src/components/inputs/Input.tsx index f896c0d7..e508cc16 100644 --- a/ui-core/src/components/inputs/Input.tsx +++ b/ui-core/src/components/inputs/Input.tsx @@ -71,6 +71,7 @@ const Input = React.forwardRef( withIcons = [], onKeyUp, onBlur, + onCloseStatusMessage, ...rest }, ref @@ -86,7 +87,9 @@ const Input = React.forwardRef( disabled={disabled} required={required} small={small} + statusIconPosition={statusWithMessage?.tooltip ? 'before-status-message' : undefined} className={cx('input-field-wrapper', inputFieldWrapperClassname)} + onCloseStatusMessage={onCloseStatusMessage} >
return ( {status === 'loading' && } - {status === 'info' && } + {status === 'info' && } {status === 'success' && } {status === 'warning' && } {status === 'error' && } diff --git a/ui-core/src/components/inputs/StatusMessage.tsx b/ui-core/src/components/inputs/StatusMessage.tsx index 56e7c42e..79871bc8 100644 --- a/ui-core/src/components/inputs/StatusMessage.tsx +++ b/ui-core/src/components/inputs/StatusMessage.tsx @@ -1,12 +1,15 @@ import React from 'react'; +import { X } from '@osrd-project/ui-icons'; import cx from 'classnames'; import InputStatusIcon from './InputStatusIcon'; export type Status = 'success' | 'info' | 'error' | 'warning' | 'loading'; +export type TooltipType = 'left' | 'right'; export type StatusWithMessage = { + tooltip?: TooltipType; status: Status; message?: string; }; @@ -15,16 +18,30 @@ export type StatusMessageProps = { statusWithMessage: StatusWithMessage; showIcon?: boolean; small?: boolean; + onClose?: () => void; }; -const StatusMessage = ({ statusWithMessage, showIcon, small }: StatusMessageProps) => { - const { status, message } = statusWithMessage; +const StatusMessage = ({ statusWithMessage, showIcon, small, onClose }: StatusMessageProps) => { + const { tooltip, status, message } = statusWithMessage; if (message === undefined) return null; return ( -
+
{showIcon && } {message} + {status === 'info' && ( + + )}
); }; diff --git a/ui-core/src/stories/Input.stories.tsx b/ui-core/src/stories/Input.stories.tsx index aa869bec..27884955 100644 --- a/ui-core/src/stories/Input.stories.tsx +++ b/ui-core/src/stories/Input.stories.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { useState } from 'react'; import { ChevronDown, X } from '@osrd-project/ui-icons'; import type { Meta, StoryObj } from '@storybook/react'; import '@osrd-project/ui-core/dist/theme.css'; import Input from '../components/inputs/Input'; +import { type StatusWithMessage } from '../components/inputs/StatusMessage'; const meta: Meta = { component: Input, @@ -173,6 +174,80 @@ export const ErrorInput: Story = { }, }; +export const TooltipErrorInput: Story = { + args: { + label: 'Name', + type: 'text', + required: true, + value: 'Michel Sardou', + statusWithMessage: { + tooltip: 'right', + status: 'error', + message: '“Michel Sardou” can’t be used', + }, + }, +}; + +export const TooltipInfoInput: Story = { + args: { + label: 'Name', + type: 'text', + required: true, + value: 'Michel Sardou', + }, + decorators: [ + function Component(Story, ctx) { + const [status, setStatus] = useState({ + tooltip: 'right', + status: 'info', + message: '“Michel Sardou” can’t be used', + }); + return ( + setStatus(undefined), + }} + /> + ); + }, + ], +}; + +export const TwoTooltipErrorInput: Story = { + decorators: [ + (Story) => ( +
+ + +
+ ), + ], +}; + export const ErrorWithoutMessageInput: Story = { args: { label: 'Name', diff --git a/ui-core/src/styles/inputs/fieldWrapper.css b/ui-core/src/styles/inputs/fieldWrapper.css index cd8112c3..9ad562b8 100644 --- a/ui-core/src/styles/inputs/fieldWrapper.css +++ b/ui-core/src/styles/inputs/fieldWrapper.css @@ -1,17 +1,25 @@ .feed-back { + --input-wrapper-padding: 3px; + --field-wrapper-padding-bottom: 16px; + --status-message-wrapper-tooltip--offset: 4px; + border-radius: 0.5rem; position: relative; padding: 0.625rem 0.813rem 1rem 1rem; - &.info { + &.success:not(:has(.status-message-wrapper-tooltip)) { + @apply bg-success-5; + } + + &.info:not(:has(.status-message-wrapper-tooltip)) { @apply bg-info-5; } - &.warning { + &.warning:not(:has(.status-message-wrapper-tooltip)) { @apply bg-warning-5; } - &.error { + &.error:not(:has(.status-message-wrapper-tooltip)) { @apply bg-error-5; } diff --git a/ui-core/src/styles/inputs/statusMessage.css b/ui-core/src/styles/inputs/statusMessage.css index b6d46948..83bd561a 100644 --- a/ui-core/src/styles/inputs/statusMessage.css +++ b/ui-core/src/styles/inputs/statusMessage.css @@ -31,3 +31,84 @@ } } } + +.status-message-wrapper-tooltip { + position: absolute; + display: flex; + align-items: center; + width: 320px; + top: calc( + 100% - var(--input-wrapper-padding) - var(--field-wrapper-padding-bottom) - + var(--status-message-wrapper-tooltip--offset) + ); + gap: 12px; + border-radius: 8px; + box-shadow: + 0 10px 20px 0 rgba(0, 0, 0, 0.2), + 0 1px 0 rgba(255, 255, 255, 0.6) inset; + padding-inline: 12px; + z-index: 10; + + .status-close { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 8px; + right: 8px; + color: theme('colors.info.30'); + &:hover { + color: theme('colors.info.80'); + } + } + + &.loading { + background-color: theme('colors.white.25'); + } + + &.success { + background-color: theme('colors.success.5'); + } + + &.error { + background-color: theme('colors.error.5'); + } + + &.info { + background-color: theme('colors.info.5'); + padding-inline: 12px 32px; + } + + &.warning { + background-color: theme('colors.warning.5'); + } + + &.tooltip-left { + left: 8px; + } + + &.tooltip-right { + right: 8px; + } + + .status-message { + font-weight: 600; + padding-block: 6px 10px; + + &.success { + color: theme('colors.success.60'); + } + + &.error { + color: theme('colors.error.60'); + } + + &.info { + color: theme('colors.info.60'); + } + + &.warning { + color: theme('colors.warning.60'); + } + } +}