Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions frontend/documentation/components/FieldError.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import FieldError from 'components/base/forms/FieldError'

const meta: Meta<typeof FieldError> = {
component: FieldError,
parameters: { layout: 'padded' },
title: 'Components/Forms/FieldError',
}
export default meta

type Story = StoryObj<typeof FieldError>

export const Default: Story = {
render: () => <FieldError error='This field is required.' />,
}

export const RichMessage: Story = {
render: () => (
<FieldError
error={
<>
Must be a valid <strong>email address</strong>.
</>
}
/>
),
}
33 changes: 33 additions & 0 deletions frontend/documentation/components/FieldLabel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import FieldLabel from 'components/base/forms/FieldLabel'

const meta: Meta<typeof FieldLabel> = {
component: FieldLabel,
parameters: { layout: 'padded' },
title: 'Components/Forms/FieldLabel',
}
export default meta

type Story = StoryObj<typeof FieldLabel>

export const Default: Story = {
render: () => <FieldLabel htmlFor='email'>Email</FieldLabel>,
}

export const Required: Story = {
render: () => (
<FieldLabel htmlFor='email' required>
Email
</FieldLabel>
),
}

export const WithTooltip: Story = {
render: () => (
<FieldLabel htmlFor='email' tooltip='We never share your email.'>
Email
</FieldLabel>
),
}
19 changes: 12 additions & 7 deletions frontend/web/components/BreadcrumbSeparator.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { FC, ReactNode, useEffect, useRef, useState } from 'react'
import React, {
FC,
KeyboardEvent,
ReactNode,
useEffect,
useRef,
useState,
} from 'react'
import { IonIcon } from '@ionic/react'
import { checkmarkCircle, chevronDown, chevronUp } from 'ionicons/icons'
import InlineModal from './InlineModal'
Expand Down Expand Up @@ -334,10 +341,8 @@ const BreadcrumbSeparator: FC<BreadcrumbSeparatorType> = ({
>
<Input
autoFocus={focus === 'organisation'}
onKeyDown={(e: KeyboardEvent) =>
navigateOrganisations(e, organisations)
}
onChange={(e: KeyboardEvent) => {
onKeyDown={(e) => navigateOrganisations(e, organisations)}
onChange={(e) => {
setOrganisationSearch(Utils.safeParseEventValue(e))
}}
search
Expand Down Expand Up @@ -390,11 +395,11 @@ const BreadcrumbSeparator: FC<BreadcrumbSeparatorType> = ({
)}
>
<Input
onChange={(e: InputEvent) => {
onChange={(e) => {
setProjectSearch(Utils.safeParseEventValue(e))
}}
autoFocus={focus === 'project'}
onKeyDown={(e: KeyboardEvent) => navigateProjects(e)}
onKeyDown={(e) => navigateProjects(e)}
search
className='full-width'
inputClassName='border-0 bg-transparent border-bottom-1'
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/ChangeRequestsSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const ChangeRequestsSetting: FC<ChangeRequestsSettingType> = ({
name='env-name'
disabled={isLoading}
min={0}
onChange={(e: InputEvent) => {
onChange={(e) => {
if (!Utils.safeParseEventValue(e)) return
onChange(parseInt(Utils.safeParseEventValue(e)))
}}
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/GroupSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const GroupSelect: FC<GroupSelectType> = ({
<Input
disabled={disabled}
value={filter}
onChange={(e: InputEvent) => setFilter(Utils.safeParseEventValue(e))}
onChange={(e) => setFilter(Utils.safeParseEventValue(e))}
className='full-width mb-2'
placeholder='Type or choose a Group'
search
Expand Down
8 changes: 2 additions & 6 deletions frontend/web/components/PermissionsTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,7 @@ const PermissionsTabs: FC<PermissionsTabsType> = ({
type='text'
className='ml-3'
value={searchProject}
onChange={(e: InputEvent) =>
setSearchProject(Utils.safeParseEventValue(e))
}
onChange={(e) => setSearchProject(Utils.safeParseEventValue(e))}
size='small'
placeholder='Search Projects'
search
Expand Down Expand Up @@ -142,9 +140,7 @@ const PermissionsTabs: FC<PermissionsTabsType> = ({
type='text'
className='ml-3'
value={searchEnv}
onChange={(e: InputEvent) =>
setSearchEnv(Utils.safeParseEventValue(e))
}
onChange={(e) => setSearchEnv(Utils.safeParseEventValue(e))}
size='small'
placeholder='Search Environments'
search
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/RolesSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const RoleSelect: FC<RoleSelectType> = ({
<Input
disabled={disabled}
value={filter}
onChange={(e: InputEvent) => setFilter(Utils.safeParseEventValue(e))}
onChange={(e) => setFilter(Utils.safeParseEventValue(e))}
className='full-width mb-2'
placeholder='Type or choose a Role'
search
Expand Down
4 changes: 2 additions & 2 deletions frontend/web/components/base/LabelWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Icon from 'components/icons/Icon'
import { FC } from 'react'
import { FC, ReactNode } from 'react'

interface LabelWithTooltipProps {
label: string
label: ReactNode
tooltip?: string
}

Expand Down
30 changes: 30 additions & 0 deletions frontend/web/components/base/forms/FieldError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { FC, ReactNode } from 'react'
import cn from 'classnames'

interface FieldErrorProps {
// The field-level error to show; renders nothing when falsy.
error?: ReactNode
// Set so the control can reference it via aria-describedby.
id?: string
className?: string
}

// Inline, per-field validation message shown beneath a control. The small-text
// counterpart to ErrorMessage (which is a banner alert for API/form-level
// errors) — use FieldError for a single field's validation.
const FieldError: FC<FieldErrorProps> = ({ className, error, id }) => {
if (!error) {
return null
}
return (
<span
id={id}
className={cn('text-danger text-small d-block mt-1', className)}
role='alert'
>
{error}
</span>
)
}

export default FieldError
46 changes: 46 additions & 0 deletions frontend/web/components/base/forms/FieldLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { FC, ReactNode } from 'react'
import cn from 'classnames'
import LabelWithTooltip from 'components/base/LabelWithTooltip'

interface FieldLabelProps {
// Associates the label with its control; required for accessibility.
htmlFor?: string
children: ReactNode
// Shows a danger asterisk after the label.
required?: boolean
// When set, an info icon follows the label and reveals this text on hover.
tooltip?: string
className?: string
}

// The label for a form field — wires `htmlFor` to the control, with an optional
// required indicator and an info-icon tooltip. The tooltip is rendered by the
// shared LabelWithTooltip (which uses the DS Tooltip), not hand-rolled here.
const FieldLabel: FC<FieldLabelProps> = ({
children,
className,
htmlFor,
required,
tooltip,
}) => (
<label
htmlFor={htmlFor}
className={cn('control-label d-flex align-items-center', className)}
>
<LabelWithTooltip
label={
<>
{children}
{required && (
<span className='text-danger ml-1' aria-hidden>
*
</span>
)}
</>
}
tooltip={tooltip}
/>
</label>
)

export default FieldLabel
Loading
Loading