@ceed/cds 1.33.0 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ import * as React from 'react';
2
+ import { type BoxProps } from '@mui/joy/Box';
3
+ export type SearchBarSlot = 'root';
4
+ export interface SearchBarOption {
5
+ label: string;
6
+ value: string;
7
+ placeholder?: string;
8
+ }
9
+ export interface SearchBarProps extends Omit<BoxProps, 'onChange'> {
10
+ showSelect?: boolean;
11
+ options?: SearchBarOption[];
12
+ placeholder?: string;
13
+ value: string;
14
+ onChange: (value: string) => void;
15
+ onSearch?: (params: {
16
+ selectValue?: string;
17
+ inputValue: string;
18
+ }) => void;
19
+ }
20
+ export type SearchBarOwnerState = Required<Pick<SearchBarProps, 'showSelect'>>;
21
+ export declare const SearchBar: React.ForwardRefExoticComponent<Omit<SearchBarProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,3 @@
1
+ import { SearchBar } from './SearchBar';
2
+ export * from './SearchBar';
3
+ export default SearchBar;
@@ -7,6 +7,7 @@ import { DateRangePickerProps } from '../DateRangePicker';
7
7
  import { MonthPickerProps } from '../MonthPicker';
8
8
  import { MonthRangePickerProps } from '../MonthRangePicker';
9
9
  import { PercentageInputProps } from '../PercentageInput';
10
+ import type { SearchBarOwnerState, SearchBarProps, SearchBarSlot } from '../SearchBar/SearchBar';
10
11
  declare module '@mui/joy/styles' {
11
12
  interface TypographySystemOverrides {
12
13
  'marketing-lg': true;
@@ -42,6 +43,10 @@ declare module '@mui/joy/styles' {
42
43
  defaultProps?: Partial<PercentageInputProps>;
43
44
  styleOverrides?: StyleOverrides<'root', {}, Theme>;
44
45
  };
46
+ SearchBar?: {
47
+ defaultProps?: Partial<SearchBarProps>;
48
+ styleOverrides?: StyleOverrides<SearchBarSlot, SearchBarOwnerState, Theme>;
49
+ };
45
50
  }
46
51
  }
47
52
  declare module '@mui/joy/Avatar' {
@@ -207,6 +207,68 @@ Full demonstration of all GFM features.
207
207
  />
208
208
  ````
209
209
 
210
+ ### External Link Target
211
+
212
+ Override the built-in anchor renderer via `markdownOptions.components.a` to detect external links by origin comparison, open them in a new tab, and prepend an `OpenInNew` icon so users can tell external links apart at a glance. Internal links keep `defaultLinkAction` behavior and render without the icon.
213
+
214
+ ```tsx
215
+ <Markdown {...args} markdownOptions={{
216
+ components: {
217
+ a: ({
218
+ href,
219
+ children
220
+ }) => {
221
+ const isExternal = (() => {
222
+ if (!href || typeof window === 'undefined') return false;
223
+ try {
224
+ return new URL(href, window.location.href).origin !== window.location.origin;
225
+ } catch {
226
+ return false;
227
+ }
228
+ })();
229
+ return <Link href={href} target={isExternal ? '_blank' : '_self'} rel={isExternal ? 'noopener noreferrer' : undefined} startDecorator={isExternal ? <OpenInNew sx={{
230
+ fontSize: '1em'
231
+ }} /> : undefined}>
232
+ {children}
233
+ </Link>;
234
+ }
235
+ }
236
+ }} />
237
+ ```
238
+
239
+ ```tsx
240
+ import OpenInNew from '@mui/icons-material/OpenInNew';
241
+ import { Markdown, Link } from '@ceed/cds';
242
+
243
+ <Markdown
244
+ content={content}
245
+ markdownOptions={{
246
+ components: {
247
+ a: ({ href, children }) => {
248
+ const isExternal = (() => {
249
+ if (!href || typeof window === 'undefined') return false;
250
+ try {
251
+ return new URL(href, window.location.href).origin !== window.location.origin;
252
+ } catch {
253
+ return false;
254
+ }
255
+ })();
256
+ return (
257
+ <Link
258
+ href={href}
259
+ target={isExternal ? '_blank' : '_self'}
260
+ rel={isExternal ? 'noopener noreferrer' : undefined}
261
+ startDecorator={isExternal ? <OpenInNew sx={{ fontSize: '1em' }} /> : undefined}
262
+ >
263
+ {children}
264
+ </Link>
265
+ );
266
+ },
267
+ },
268
+ }}
269
+ />
270
+ ```
271
+
210
272
  ## When to Use
211
273
 
212
274
  ### ✅ Good Use Cases
@@ -45,6 +45,7 @@ export { PercentageInput } from './PercentageInput';
45
45
  export { Radio, RadioGroup } from './Radio';
46
46
  export { RadioTileGroup } from './RadioTileGroup';
47
47
  export { RadioList } from './RadioList';
48
+ export { SearchBar, type SearchBarProps, type SearchBarOwnerState, type SearchBarSlot, type SearchBarOption, } from './SearchBar';
48
49
  export { Select, Option } from './Select';
49
50
  export { Sheet } from './Sheet';
50
51
  export { Stack } from './Stack';
@@ -0,0 +1,180 @@
1
+ # SearchBar
2
+
3
+ ## Introduction
4
+
5
+ A search input component combining a text field and a search button. Optionally includes a category Select that lets users narrow results by keyword type. Hovering over the input while it has a value reveals a clear (✕) button to reset the field. Sizes to its content by default (`inline-flex`) and accepts all `Box` props for layout control.
6
+
7
+ ```tsx
8
+ <SearchBar value={value} onChange={setValue} />
9
+ ```
10
+
11
+ | Field | Description | Default |
12
+ | ----------- | ----------- | ------- |
13
+ | showSelect | — | — |
14
+ | options | — | — |
15
+ | placeholder | — | — |
16
+ | value | — | — |
17
+ | onChange | — | — |
18
+ | onSearch | — | — |
19
+
20
+ ## Usage
21
+
22
+ ```tsx
23
+ import { SearchBar } from '@ceed/cds';
24
+
25
+ // Basic
26
+ <SearchBar value={query} onChange={setQuery} onSearch={({ inputValue }) => fetch(inputValue)} />
27
+
28
+ // With category Select
29
+ <SearchBar
30
+ showSelect
31
+ options={[
32
+ { label: 'Account #', value: 'account', placeholder: 'e.g. 1234567' },
33
+ { label: 'Jira Issue #', value: 'jira', placeholder: 'e.g. PROC-1234' },
34
+ ]}
35
+ value={query}
36
+ onChange={setQuery}
37
+ onSearch={({ selectValue, inputValue }) => fetch(selectValue, inputValue)}
38
+ />
39
+ ```
40
+
41
+ ## With Select
42
+
43
+ Use `showSelect` together with `options` to display a category Select to the left of the text input. The selected category is passed as `selectValue` in the `onSearch` callback.
44
+
45
+ ```tsx
46
+ <SearchBar showSelect options={SAMPLE_OPTIONS} value={value} onChange={setValue} />
47
+ ```
48
+
49
+ ## Clearable Input
50
+
51
+ When the input has a value, hovering over the component reveals a clear (✕) button at the right of the text field. Clicking it calls `onChange('')` without triggering `onSearch`.
52
+
53
+ ```tsx
54
+ <Stack alignItems="flex-start" gap={2}>
55
+ <Typography level="body-sm">Hover over the input to reveal the clear (✕) button.</Typography>
56
+ <SearchBar value={value} onChange={setValue} />
57
+ </Stack>
58
+ ```
59
+
60
+ ## Placeholder
61
+
62
+ Each `SearchBarOption` can include a `placeholder` field that is shown in the text input while that category is selected. Pass a `placeholder` prop directly to override the option-level placeholder for all categories.
63
+
64
+ ```tsx
65
+ <Stack alignItems="flex-start" gap={3}>
66
+ <Stack alignItems="flex-start" gap={1}>
67
+ <Typography level="body-xs" fontWeight="md">
68
+ No placeholder prop — uses the active option's placeholder
69
+ </Typography>
70
+ <SearchBar showSelect options={SAMPLE_OPTIONS} value={value} onChange={setValue} />
71
+ </Stack>
72
+ <Stack alignItems="flex-start" gap={1}>
73
+ <Typography level="body-xs" fontWeight="md">
74
+ placeholder="Search by keyword" — overrides option-level placeholder
75
+ </Typography>
76
+ <SearchBar showSelect options={SAMPLE_OPTIONS} placeholder="Search by keyword" value={value} onChange={setValue} />
77
+ </Stack>
78
+ </Stack>
79
+ ```
80
+
81
+ ## onSearch
82
+
83
+ `onSearch` fires when the search button is clicked or the Enter key is pressed. It receives `inputValue` (always present) and `selectValue` (only present when `showSelect` is `true`).
84
+
85
+ ```tsx
86
+ <Stack alignItems="flex-start" gap={2}>
87
+ <SearchBar showSelect options={SAMPLE_OPTIONS} value={value} onChange={setValue} onSearch={setLastSearch} />
88
+ <Typography level="body-sm">value: "{value}"</Typography>
89
+ {lastSearch && <Typography level="body-sm">
90
+ onSearch: [{lastSearch.selectValue}] "{lastSearch.inputValue}"
91
+ </Typography>}
92
+ </Stack>
93
+ ```
94
+
95
+ When `showSelect` is `false`, `selectValue` is omitted from the payload entirely — not `undefined` as a key, but absent.
96
+
97
+ ```tsx
98
+ <Stack alignItems="flex-start" gap={2}>
99
+ <Typography level="body-sm">
100
+ When <code>showSelect</code> is <code>false</code>, <code>selectValue</code> is omitted from the{' '}
101
+ <code>onSearch</code> payload.
102
+ </Typography>
103
+ <SearchBar value={value} onChange={setValue} onSearch={setLastSearch} />
104
+ {lastSearch && <Stack alignItems="flex-start" gap={0.5}>
105
+ <Typography level="body-sm">inputValue: "{lastSearch.inputValue}"</Typography>
106
+ <Typography level="body-sm" sx={{
107
+ color: lastSearch.selectValue === undefined ? 'success.500' : 'danger.500'
108
+ }}>
109
+ selectValue: {lastSearch.selectValue === undefined ? 'undefined ✅' : `"${lastSearch.selectValue}" ❌`}
110
+ </Typography>
111
+ </Stack>}
112
+ </Stack>
113
+ ```
114
+
115
+ ## Width
116
+
117
+ SearchBar sizes to its content by default. Pass any `Box` width prop to constrain or stretch it.
118
+
119
+ ```tsx
120
+ <Stack alignItems="flex-start" gap={3}>
121
+ <Stack alignItems="flex-start" gap={1}>
122
+ <Typography level="body-xs" fontWeight="md">
123
+ width="100%" — stretches to parent width
124
+ </Typography>
125
+ <Box sx={{
126
+ border: '1px dashed',
127
+ borderColor: 'neutral.300',
128
+ padding: 1
129
+ }}>
130
+ <SearchBar width="100%" value={value} onChange={setValue} />
131
+ </Box>
132
+ </Stack>
133
+ <Stack alignItems="flex-start" gap={1}>
134
+ <Typography level="body-xs" fontWeight="md">
135
+ width={400} — fixed 400px
136
+ </Typography>
137
+ <SearchBar width={400} value={value} onChange={setValue} />
138
+ </Stack>
139
+ <Stack alignItems="flex-start" gap={1}>
140
+ <Typography level="body-xs" fontWeight="md">
141
+ No width prop — sizes to content (inline-flex default)
142
+ </Typography>
143
+ <SearchBar value={value} onChange={setValue} />
144
+ </Stack>
145
+ </Stack>
146
+ ```
147
+
148
+ ## Props and Customization
149
+
150
+ | Prop | Type | Default | Description |
151
+ | ------------- | ---------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------- |
152
+ | `value` | `string` | — | Current value of the text input. Required. |
153
+ | `onChange` | `(value: string) => void` | — | Called when the text input value changes. Required. |
154
+ | `onSearch` | `(params: { selectValue?: string; inputValue: string }) => void` | — | Called on search button click or Enter key. |
155
+ | `showSelect` | `boolean` | `false` | Show the category Select alongside the input. |
156
+ | `options` | `SearchBarOption[]` | — | Category options for the Select. Required when `showSelect` is `true`. |
157
+ | `placeholder` | `string` | — | Placeholder text for the input. Takes priority over the option-level `placeholder`. |
158
+
159
+ > **Note**: Also accepts all `Box` props (`width`, `sx`, `className`, `style`, etc.).
160
+
161
+ ### SearchBarOption
162
+
163
+ | Field | Type | Required | Description |
164
+ | ------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
165
+ | `label` | `string` | ✓ | Display text shown in the Select dropdown. |
166
+ | `value` | `string` | ✓ | Identifier passed as `selectValue` in the `onSearch` callback. |
167
+ | `placeholder` | `string` | — | Hint text shown in the input while this category is selected (e.g. `"e.g. PROC-1234"`). Overridden by the top-level `placeholder` prop. |
168
+
169
+ ## Best Practices
170
+
171
+ - Always provide `options` when `showSelect` is `true` — the Select renders nothing if `options` is omitted.
172
+ - Add a `placeholder` to each option to show the expected input format for that category.
173
+ - Use the top-level `placeholder` prop only when a single hint applies regardless of category.
174
+ - Prefer handling `onSearch` over `onChange` for server-side queries — fire the request once on explicit submission rather than on every keystroke.
175
+
176
+ ## Accessibility
177
+
178
+ - The search button has `aria-label="Search"` and the clear button has `aria-label="Clear"`.
179
+ - The text input is a native `<input>` element; pressing Enter triggers `onSearch`.
180
+ - The clear button uses `onMouseDown` with `e.preventDefault()` to prevent the input from losing focus when clicked.
@@ -20,6 +20,7 @@
20
20
  - [Radio](./RadioButton.md)
21
21
  - [RadioList](./RadioList.md)
22
22
  - [RadioTileGroup](./RadioTileGroup.md)
23
+ - [SearchBar](./SearchBar.md)
23
24
  - [Select](./Select.md)
24
25
  - [Slider](./Slider.md)
25
26
  - [Switch](./Switch.md)