Skip to content
Merged
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
5 changes: 5 additions & 0 deletions apps/website/src/components/docs-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ function getNavigationItems(locale: Locale): DocsNavigationItem[] {
id: "components/form-input",
title: "Form input",
},
{
href: localizedPath(locale, "/docs/components/form-input-group"),
id: "components/form-input-group",
title: "Form input group",
},
{
href: localizedPath(locale, "/docs/components/form-password-input"),
id: "components/form-password-input",
Expand Down
119 changes: 119 additions & 0 deletions docs/en/components/form-input-group.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: Form input group
description: Combines a form control with related text, buttons, or a dropdown.
---

## Usage

```tsx
import { GlFormInput } from "gitlab-ui-react/form-input";
import {
GlFormInputGroup,
GlFormInputGroupAddon,
GlInputGroupText,
} from "gitlab-ui-react/form-input-group";
```

```tsx
<label id="username-label" htmlFor="username">Username</label>
<GlFormInputGroup aria-labelledby="username-label">
<GlFormInputGroupAddon position="prepend">
<GlInputGroupText id="username-prefix">@</GlInputGroupText>
</GlFormInputGroupAddon>
<GlFormInput id="username" aria-describedby="username-prefix" />
</GlFormInputGroup>
```

## Default

Compose a control explicitly inside `GlFormInputGroup`; the group does not create an input. Wrap a text prefix in `GlFormInputGroupAddon` and `GlInputGroupText`, and give the input a visible label.

<DocsExample
filename="form-input-group/FormInputGroupExample.tsx"
title="Input with a text prefix"
storybookId="ui-base-form-input-group--prepend-text"/>

## Text addons

Add text before or after an input to communicate a fixed prefix, suffix, or unit. Place a `prepend` addon before the control and an `append` addon after it in JSX: `position` selects the styling, and the group preserves child order. Keep controls and addon wrappers directly inside the group so their borders join correctly.

<DocsExample
filename="form-input-group/FormInputGroupTextExample.tsx"
title="Text prefix and suffix"/>

## Button addons

Place a related action inside an addon without wrapping the button in `GlInputGroupText`. Set its `type` when it should submit or reset a form; `GlButton` defaults to `type="button"`. This example uses the native form reset behavior to clear the search input.

<DocsExample
filename="form-input-group/FormInputGroupButtonExample.tsx"
title="Input with a clear button"/>

## Select control

Compose `GlFormSelect` when the value comes from a fixed list. Render it directly inside the group; its built-in wrapper handles the joined borders and flexible width.

<DocsExample
filename="form-input-group/FormInputGroupSelectExample.tsx"
title="Select with a text addon"/>

## Range input

A range input can use text addons to show its bounds. Set `type`, `min`, and `max` on `GlFormInput`, and keep a visible label that describes the value being adjusted.

<DocsExample
filename="form-input-group/FormInputGroupRangeExample.tsx"
title="Range with visible bounds"
storybookId="ui-base-form-input-group--range-input"/>

## States

Set `readOnly`, `disabled`, and validation `state` on the composed control. Set `disabled` separately on related actions when they should also be unavailable. A read-only input remains focusable and selectable; an invalid input needs visible feedback associated with `aria-describedby`.

<DocsExample
filename="form-input-group/FormInputGroupStatesExample.tsx"
title="Read-only, invalid, and disabled controls"/>

## Predefined options

Compose a `GlListbox` inside an addon and control the input value in React to populate it from predefined options. Apply `readOnly` to the input when editing is unavailable, and call `event.currentTarget.select()` in its `onClick` handler to select the text. These behaviors are supplied by the composed components and event handler.

<DocsExample
filename="form-input-group/FormInputGroupOptionsExample.tsx"
title="Predefined URLs and selection on click"
storybookId="ui-base-form-input-group--predefined-options-with-listbox"/>

## Accessibility

- Associate each input or select with a visible `<label>` using matching `htmlFor` and `id` values. `GlInputGroupText` renders a `div`, so its text does not label a control automatically.
- The root always renders `role="group"`. Use `aria-labelledby` or `aria-label` to name the group; that name does not label its individual controls.
- Reference meaningful prefixes, suffixes, units, and feedback from the control with `aria-describedby`.
- Give every interactive addon its own accessible name. Icon-only buttons need `aria-label`.
- Keep JSX order consistent with visual and keyboard focus order, and preserve visible focus indicators.
- Pair `state={false}` on the input with an explanation of how to fix the value; the input sets `aria-invalid` automatically.

## API

All three components forward supported `div` attributes and a ref to their rendered `div`. Input values, input events, validation, and disabled state belong to the composed controls.

### `GlFormInputGroup`

Accepts `children` and native `div` attributes, except `role`. It renders children in the supplied order and always sets `role="group"`.

### `GlFormInputGroupAddon`

| Prop | Description | Default |
| --- | --- | --- |
| `position` | Required. Selects `prepend` or `append` styling; it does not reorder children. | — |

Accepts `children` for text, buttons, or a dropdown, plus native `div` attributes.

### `GlInputGroupText`

Accepts `children` and native `div` attributes. Renders non-interactive text with the addon appearance; place it inside `GlFormInputGroupAddon`.

## Related resources

- [Form input group in the Pajamas Design System](https://design.gitlab.com/components/form-input-group/)
- [Form input group stories in Storybook](https://glui-story.nocp.space/?path=/docs/ui-base-form-input-group--docs)
- [Form input group source and complete TypeScript props](https://github.com/nocpiun/gitlab-ui-react/blob/main/packages/ui/src/base/form-input-group/form-input-group.tsx)
119 changes: 119 additions & 0 deletions docs/zh/components/form-input-group.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: Form input group
description: 将表单控件与相关文字、按钮或下拉菜单组合在一起。
---

## 用法

```tsx
import { GlFormInput } from "gitlab-ui-react/form-input";
import {
GlFormInputGroup,
GlFormInputGroupAddon,
GlInputGroupText,
} from "gitlab-ui-react/form-input-group";
```

```tsx
<label id="username-label" htmlFor="username">Username</label>
<GlFormInputGroup aria-labelledby="username-label">
<GlFormInputGroupAddon position="prepend">
<GlInputGroupText id="username-prefix">@</GlInputGroupText>
</GlFormInputGroupAddon>
<GlFormInput id="username" aria-describedby="username-prefix" />
</GlFormInputGroup>
```

## 默认

显式将控件放入 `GlFormInputGroup`,分组本身不会创建输入框。使用 `GlFormInputGroupAddon` 和 `GlInputGroupText` 包裹文字前缀,并为输入框提供可见标签。

<DocsExample
filename="form-input-group/FormInputGroupExample.tsx"
title="带文字前缀的输入框"
storybookId="ui-base-form-input-group--prepend-text"/>

## 文字附加内容

在输入框前后添加文字,表示固定前缀、后缀或单位。在 JSX 中将 `prepend` 附加内容放在控件前,将 `append` 附加内容放在控件后:`position` 选择样式,分组保留子元素顺序。将控件和附加内容容器直接放在分组内,以正确拼接边框。

<DocsExample
filename="form-input-group/FormInputGroupTextExample.tsx"
title="文字前缀与后缀"/>

## 按钮附加内容

将相关操作放入附加内容容器,无需用 `GlInputGroupText` 包裹按钮。需要提交或重置表单时设置按钮的 `type`;`GlButton` 默认为 `type="button"`。此示例通过原生表单重置行为清空搜索输入框。

<DocsExample
filename="form-input-group/FormInputGroupButtonExample.tsx"
title="带清空按钮的输入框"/>

## 选择控件

当值来自固定列表时组合使用 `GlFormSelect`。将其直接放在分组内,组件自带的容器会处理边框拼接和弹性宽度。

<DocsExample
filename="form-input-group/FormInputGroupSelectExample.tsx"
title="带文字附加内容的选择控件"/>

## 范围输入

范围输入可以通过文字附加内容显示上下限。在 `GlFormInput` 上设置 `type`、`min` 和 `max`,并提供描述所调整数值的可见标签。

<DocsExample
filename="form-input-group/FormInputGroupRangeExample.tsx"
title="显示上下限的范围输入"
storybookId="ui-base-form-input-group--range-input"/>

## 状态

在组合的控件上设置 `readOnly`、`disabled` 和验证 `state`。相关操作也应不可用时,单独为其设置 `disabled`。只读输入框仍可聚焦和选择文字;无效输入需要通过 `aria-describedby` 关联可见反馈。

<DocsExample
filename="form-input-group/FormInputGroupStatesExample.tsx"
title="只读、无效和禁用控件"/>

## 预设选项

在附加内容容器中组合 `GlListbox`,并在 React 中控制输入值,从预设选项填充输入框。不允许编辑时为输入框设置 `readOnly`,在其 `onClick` 回调中调用 `event.currentTarget.select()` 可选中文字。这些行为由组合的组件和事件回调提供。

<DocsExample
filename="form-input-group/FormInputGroupOptionsExample.tsx"
title="预设 URL 与点击选中文字"
storybookId="ui-base-form-input-group--predefined-options-with-listbox"/>

## 无障碍

- 使用相同的 `htmlFor` 与 `id` 将每个输入框或选择控件关联到可见 `<label>`。`GlInputGroupText` 渲染为 `div`,其中的文字不会自动成为控件标签。
- 根元素始终渲染 `role="group"`。使用 `aria-labelledby` 或 `aria-label` 为分组命名;分组名称不会为其中的各个控件提供标签。
- 通过控件的 `aria-describedby` 关联有意义的前缀、后缀、单位和反馈。
- 为每个交互式附加内容提供独立的无障碍名称。仅含图标的按钮需要 `aria-label`。
- 保持 JSX 顺序、视觉顺序与键盘焦点顺序一致,并保留可见焦点指示。
- 为输入框的 `state={false}` 提供说明如何修正值的反馈;输入框会自动设置 `aria-invalid`。

## API

这三个组件都将受支持的 `div` 属性和 ref 传递给渲染的 `div`。输入值、输入事件、验证和禁用状态由组合的控件管理。

### `GlFormInputGroup`

接受 `children` 和除 `role` 之外的原生 `div` 属性。按传入顺序渲染子元素,并始终设置 `role="group"`。

### `GlFormInputGroupAddon`

| 属性 | 说明 | 默认值 |
| --- | --- | --- |
| `position` | 必填。选择 `prepend` 或 `append` 样式,不会重排子元素。 | — |

接受包含文字、按钮或下拉菜单的 `children`,以及原生 `div` 属性。

### `GlInputGroupText`

接受 `children` 和原生 `div` 属性。以附加内容外观渲染非交互文字,应放入 `GlFormInputGroupAddon`。

## 相关资源

- [Pajamas 设计系统中的 Form input group](https://design.gitlab.com/components/form-input-group/)
- [Storybook 中的 Form input group stories](https://glui-story.nocp.space/?path=/docs/ui-base-form-input-group--docs)
- [Form input group 源码和完整 TypeScript 属性](https://github.com/nocpiun/gitlab-ui-react/blob/main/packages/ui/src/base/form-input-group/form-input-group.tsx)
25 changes: 25 additions & 0 deletions examples/form-input-group/FormInputGroupButtonExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { GlButton } from "gitlab-ui-react/button";
import { GlFormInput } from "gitlab-ui-react/form-input";
import {
GlFormInputGroup,
GlFormInputGroupAddon,
} from "gitlab-ui-react/form-input-group";

export default function FormInputGroupButtonExample() {
return (
<form className="max-w-md">
<label
className="mb-2 block font-bold"
htmlFor="grouped-search"
id="grouped-search-label">
Search projects
</label>
<GlFormInputGroup aria-labelledby="grouped-search-label">
<GlFormInput id="grouped-search" type="search" />
<GlFormInputGroupAddon position="append">
<GlButton type="reset">Clear</GlButton>
</GlFormInputGroupAddon>
</GlFormInputGroup>
</form>
);
}
28 changes: 28 additions & 0 deletions examples/form-input-group/FormInputGroupExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { GlFormInput } from "gitlab-ui-react/form-input";
import {
GlFormInputGroup,
GlFormInputGroupAddon,
GlInputGroupText,
} from "gitlab-ui-react/form-input-group";

export default function FormInputGroupExample() {
return (
<div className="max-w-md">
<label
className="mb-2 block font-bold"
htmlFor="grouped-username"
id="grouped-username-label">
Username
</label>
<GlFormInputGroup aria-labelledby="grouped-username-label">
<GlFormInputGroupAddon position="prepend">
<GlInputGroupText id="grouped-username-prefix">@</GlInputGroupText>
</GlFormInputGroupAddon>
<GlFormInput
aria-describedby="grouped-username-prefix"
defaultValue="NriotHrreion"
id="grouped-username" />
</GlFormInputGroup>
</div>
);
}
46 changes: 46 additions & 0 deletions examples/form-input-group/FormInputGroupOptionsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from "react";
import { GlFormInput } from "gitlab-ui-react/form-input";
import {
GlFormInputGroup,
GlFormInputGroupAddon,
} from "gitlab-ui-react/form-input-group";
import {
GlListbox,
GlListboxContent,
GlListboxItem,
GlListboxTrigger,
type GlListboxValue,
} from "gitlab-ui-react/listbox";

export default function FormInputGroupOptionsExample() {
const [value, setValue] = useState<GlListboxValue>("https://embed.example");

return (
<div className="max-w-md">
<label
className="mb-2 block font-bold"
htmlFor="grouped-selected-url"
id="grouped-selected-url-label">
Selected URL
</label>
<GlFormInputGroup aria-labelledby="grouped-selected-url-label">
<GlFormInputGroupAddon position="prepend">
<GlListbox value={value} onValueChange={setValue}>
<GlListboxTrigger>
{value === "https://embed.example" ? "Embed" : "Share"}
</GlListboxTrigger>
<GlListboxContent aria-label="URL type">
<GlListboxItem value="https://embed.example">Embed</GlListboxItem>
<GlListboxItem value="https://share.example">Share</GlListboxItem>
</GlListboxContent>
</GlListbox>
</GlFormInputGroupAddon>
<GlFormInput
id="grouped-selected-url"
onClick={(event) => event.currentTarget.select()}
readOnly
value={value ?? ""} />
</GlFormInputGroup>
</div>
);
}
33 changes: 33 additions & 0 deletions examples/form-input-group/FormInputGroupRangeExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { GlFormInput } from "gitlab-ui-react/form-input";
import {
GlFormInputGroup,
GlFormInputGroupAddon,
GlInputGroupText,
} from "gitlab-ui-react/form-input-group";

export default function FormInputGroupRangeExample() {
return (
<div className="max-w-md">
<label
className="mb-2 block font-bold"
htmlFor="grouped-progress"
id="grouped-progress-label">
Progress
</label>
<GlFormInputGroup aria-labelledby="grouped-progress-label">
<GlFormInputGroupAddon position="prepend">
<GlInputGroupText>0</GlInputGroupText>
</GlFormInputGroupAddon>
<GlFormInput
defaultValue={50}
id="grouped-progress"
max={100}
min={0}
type="range" />
<GlFormInputGroupAddon position="append">
<GlInputGroupText>100</GlInputGroupText>
</GlFormInputGroupAddon>
</GlFormInputGroup>
</div>
);
}
Loading