fluentui-extended 0.1.0
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.
- package/README.md +239 -0
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +515 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +493 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# FluentUI-Extended
|
|
2
|
+
|
|
3
|
+
Extended components for Fluent UI v9, designed to match Dynamics 365 patterns.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/fluentui-extended)
|
|
6
|
+
[](https://github.com/garethcheyne/npm-fluentui-extended/actions)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install fluentui-extended @fluentui/react-components @fluentui/react-icons
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Components
|
|
15
|
+
|
|
16
|
+
### Lookup
|
|
17
|
+
|
|
18
|
+
A searchable dropdown component styled after Dynamics 365 lookup fields. Supports async search, expandable option details, and customizable header/footer.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { Lookup, LookupOption } from 'fluentui-extended';
|
|
24
|
+
import { FluentProvider, webLightTheme } from '@fluentui/react-components';
|
|
25
|
+
|
|
26
|
+
const options: LookupOption[] = [
|
|
27
|
+
{ key: '1', text: 'Contoso Ltd', secondaryText: 'CON001' },
|
|
28
|
+
{ key: '2', text: 'Fabrikam Inc', secondaryText: 'FAB001' },
|
|
29
|
+
{ key: '3', text: 'Adventure Works', secondaryText: 'ADV001' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
function App() {
|
|
33
|
+
const [selected, setSelected] = useState<LookupOption | null>(null);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<FluentProvider theme={webLightTheme}>
|
|
37
|
+
<Lookup
|
|
38
|
+
options={options}
|
|
39
|
+
selectedOption={selected}
|
|
40
|
+
onOptionSelect={setSelected}
|
|
41
|
+
placeholder="Search accounts..."
|
|
42
|
+
/>
|
|
43
|
+
</FluentProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
### Basic Selection (with key)
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
|
54
|
+
|
|
55
|
+
<Lookup
|
|
56
|
+
options={options}
|
|
57
|
+
selectedKey={selectedKey}
|
|
58
|
+
onOptionSelect={(opt) => setSelectedKey(opt?.key ?? null)}
|
|
59
|
+
placeholder="Search..."
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Async Search (API Integration)
|
|
64
|
+
|
|
65
|
+
For async scenarios, use `selectedOption` to persist the display value when options change:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
function AsyncLookup() {
|
|
69
|
+
const [options, setOptions] = useState<LookupOption[]>([]);
|
|
70
|
+
const [selected, setSelected] = useState<LookupOption | null>(null);
|
|
71
|
+
const [loading, setLoading] = useState(false);
|
|
72
|
+
|
|
73
|
+
const handleSearch = async (searchText: string) => {
|
|
74
|
+
setLoading(true);
|
|
75
|
+
const response = await fetch(`/api/accounts?search=${searchText}`);
|
|
76
|
+
setOptions(await response.json());
|
|
77
|
+
setLoading(false);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Lookup
|
|
82
|
+
options={options}
|
|
83
|
+
selectedOption={selected}
|
|
84
|
+
onOptionSelect={setSelected}
|
|
85
|
+
onSearchChange={handleSearch}
|
|
86
|
+
loading={loading}
|
|
87
|
+
searchDebounceMs={300}
|
|
88
|
+
placeholder="Type to search..."
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### With Icons and Expandable Details
|
|
95
|
+
|
|
96
|
+
Options can include icons and expandable detail rows (click chevron to expand):
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { BuildingRegular } from '@fluentui/react-icons';
|
|
100
|
+
|
|
101
|
+
const options: LookupOption[] = [
|
|
102
|
+
{
|
|
103
|
+
key: '1',
|
|
104
|
+
text: 'Contoso Ltd',
|
|
105
|
+
secondaryText: 'CON001',
|
|
106
|
+
icon: <BuildingRegular />,
|
|
107
|
+
details: [
|
|
108
|
+
{ label: 'Phone', value: '555-0100' },
|
|
109
|
+
{ label: 'Industry', value: 'Technology' },
|
|
110
|
+
{ value: 'Active Customer' },
|
|
111
|
+
],
|
|
112
|
+
data: { id: 'acc-001', revenue: 5000000 }, // Custom data accessible in onOptionSelect
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Dynamics 365 Style (Header & Footer)
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { Text, Button, Link } from '@fluentui/react-components';
|
|
121
|
+
import { AddRegular, PersonSearchRegular } from '@fluentui/react-icons';
|
|
122
|
+
|
|
123
|
+
<Lookup
|
|
124
|
+
options={options}
|
|
125
|
+
selectedOption={selected}
|
|
126
|
+
onOptionSelect={setSelected}
|
|
127
|
+
header={
|
|
128
|
+
<>
|
|
129
|
+
<Text size={200}>Accounts</Text>
|
|
130
|
+
<Button appearance="outline" size="small">Recent records</Button>
|
|
131
|
+
</>
|
|
132
|
+
}
|
|
133
|
+
footer={
|
|
134
|
+
<>
|
|
135
|
+
<Link style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
136
|
+
<AddRegular /> New
|
|
137
|
+
</Link>
|
|
138
|
+
<Link style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
139
|
+
<PersonSearchRegular /> Advanced
|
|
140
|
+
</Link>
|
|
141
|
+
</>
|
|
142
|
+
}
|
|
143
|
+
/>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## API Reference
|
|
147
|
+
|
|
148
|
+
### Lookup Props
|
|
149
|
+
|
|
150
|
+
| Prop | Type | Default | Description |
|
|
151
|
+
|------|------|---------|-------------|
|
|
152
|
+
| `options` | `LookupOption[]` | `[]` | Options to display in the dropdown |
|
|
153
|
+
| `selectedKey` | `string \| null` | - | Selected option key (controlled) |
|
|
154
|
+
| `selectedOption` | `LookupOption \| null` | - | Selected option object (recommended for async) |
|
|
155
|
+
| `onOptionSelect` | `(option: LookupOption \| null) => void` | - | Selection change callback |
|
|
156
|
+
| `onSearchChange` | `(searchText: string) => void` | - | Search text change callback |
|
|
157
|
+
| `placeholder` | `string` | `'Search...'` | Input placeholder |
|
|
158
|
+
| `loading` | `boolean` | `false` | Show loading spinner |
|
|
159
|
+
| `noResultsMessage` | `string` | `'No results found'` | Empty state message |
|
|
160
|
+
| `clearable` | `boolean` | `true` | Show clear button |
|
|
161
|
+
| `minSearchLength` | `number` | `0` | Min chars before search fires |
|
|
162
|
+
| `searchDebounceMs` | `number` | `300` | Search debounce delay (ms) |
|
|
163
|
+
| `header` | `ReactNode` | - | Header content |
|
|
164
|
+
| `footer` | `ReactNode` | - | Footer content |
|
|
165
|
+
| `disabled` | `boolean` | `false` | Disable the lookup |
|
|
166
|
+
|
|
167
|
+
*Also accepts all Fluent UI `Input` props except `onChange` and `value`.*
|
|
168
|
+
|
|
169
|
+
### LookupOption
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
interface LookupOption {
|
|
173
|
+
key: string; // Unique identifier (required)
|
|
174
|
+
text: string; // Display text (required)
|
|
175
|
+
secondaryText?: string; // Secondary line of text
|
|
176
|
+
icon?: ReactNode; // Icon component
|
|
177
|
+
details?: LookupOptionDetail[]; // Expandable details (chevron appears)
|
|
178
|
+
data?: unknown; // Custom data payload
|
|
179
|
+
disabled?: boolean; // Disable this option
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface LookupOptionDetail {
|
|
183
|
+
label?: string; // Optional label (e.g., "Phone:")
|
|
184
|
+
value: string; // Detail value
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Keyboard Navigation
|
|
189
|
+
|
|
190
|
+
| Key | Action |
|
|
191
|
+
|-----|--------|
|
|
192
|
+
| `↓` | Open dropdown / Move to next option |
|
|
193
|
+
| `↑` | Move to previous option |
|
|
194
|
+
| `Enter` | Select highlighted option |
|
|
195
|
+
| `Escape` | Close dropdown |
|
|
196
|
+
| `Tab` | Close dropdown and move focus |
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Install dependencies
|
|
202
|
+
npm install
|
|
203
|
+
|
|
204
|
+
# Run demo app
|
|
205
|
+
npm run demo
|
|
206
|
+
|
|
207
|
+
# Build library
|
|
208
|
+
npm run build
|
|
209
|
+
|
|
210
|
+
# Type check
|
|
211
|
+
npm run typecheck
|
|
212
|
+
|
|
213
|
+
# Watch mode
|
|
214
|
+
npm run dev
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## GitHub Actions
|
|
218
|
+
|
|
219
|
+
This project includes CI/CD workflows:
|
|
220
|
+
|
|
221
|
+
- **CI** (`ci.yml`) - Runs on push/PR to main: type checking and build
|
|
222
|
+
- **Release** (`release.yml`) - Publishes to npm when a GitHub release is created
|
|
223
|
+
|
|
224
|
+
### Setup npm Publishing
|
|
225
|
+
|
|
226
|
+
1. Create an npm access token at [npmjs.com](https://www.npmjs.com/settings/~/tokens)
|
|
227
|
+
2. Add it as a repository secret named `NPM_TOKEN` in GitHub Settings → Secrets
|
|
228
|
+
|
|
229
|
+
## Contributing
|
|
230
|
+
|
|
231
|
+
1. Fork the repository
|
|
232
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
233
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
234
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
235
|
+
5. Open a Pull Request
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MITMIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { InputProps } from '@fluentui/react-components';
|
|
3
|
+
|
|
4
|
+
interface LookupOptionDetail {
|
|
5
|
+
/** Label for the detail row */
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Value for the detail row */
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
interface LookupOption {
|
|
11
|
+
/** Unique identifier for the option */
|
|
12
|
+
key: string;
|
|
13
|
+
/** Display text for the option */
|
|
14
|
+
text: string;
|
|
15
|
+
/** Optional secondary text */
|
|
16
|
+
secondaryText?: string;
|
|
17
|
+
/** Optional icon to display */
|
|
18
|
+
icon?: React.ReactNode;
|
|
19
|
+
/** Optional expandable details */
|
|
20
|
+
details?: LookupOptionDetail[];
|
|
21
|
+
/** Optional data associated with the option */
|
|
22
|
+
data?: unknown;
|
|
23
|
+
/** Whether the option is disabled */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface LookupProps extends Omit<InputProps, 'onChange' | 'value'> {
|
|
27
|
+
/** The options to display in the dropdown */
|
|
28
|
+
options?: LookupOption[];
|
|
29
|
+
/** Currently selected option key (use with options array lookup) */
|
|
30
|
+
selectedKey?: string | null;
|
|
31
|
+
/** Currently selected option (use for controlled selection with full data) */
|
|
32
|
+
selectedOption?: LookupOption | null;
|
|
33
|
+
/** Callback when selection changes - receives full option with data */
|
|
34
|
+
onOptionSelect?: (option: LookupOption | null) => void;
|
|
35
|
+
/** Callback when search text changes - use for async loading */
|
|
36
|
+
onSearchChange?: (searchText: string) => void;
|
|
37
|
+
/** Placeholder text when no selection */
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
/** Whether the lookup is loading options */
|
|
40
|
+
loading?: boolean;
|
|
41
|
+
/** Message to display when no results found */
|
|
42
|
+
noResultsMessage?: string;
|
|
43
|
+
/** Whether to allow clearing the selection */
|
|
44
|
+
clearable?: boolean;
|
|
45
|
+
/** Minimum characters before triggering search */
|
|
46
|
+
minSearchLength?: number;
|
|
47
|
+
/** Debounce delay for search in milliseconds */
|
|
48
|
+
searchDebounceMs?: number;
|
|
49
|
+
/** Whether the dropdown should match the input width */
|
|
50
|
+
matchInputWidth?: boolean;
|
|
51
|
+
/** Header content rendered at the top of the dropdown */
|
|
52
|
+
header?: React.ReactNode;
|
|
53
|
+
/** Footer content rendered at the bottom of the dropdown */
|
|
54
|
+
footer?: React.ReactNode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare const Lookup: React.FC<LookupProps>;
|
|
58
|
+
|
|
59
|
+
export { Lookup, type LookupOption, type LookupOptionDetail, type LookupProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { InputProps } from '@fluentui/react-components';
|
|
3
|
+
|
|
4
|
+
interface LookupOptionDetail {
|
|
5
|
+
/** Label for the detail row */
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Value for the detail row */
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
interface LookupOption {
|
|
11
|
+
/** Unique identifier for the option */
|
|
12
|
+
key: string;
|
|
13
|
+
/** Display text for the option */
|
|
14
|
+
text: string;
|
|
15
|
+
/** Optional secondary text */
|
|
16
|
+
secondaryText?: string;
|
|
17
|
+
/** Optional icon to display */
|
|
18
|
+
icon?: React.ReactNode;
|
|
19
|
+
/** Optional expandable details */
|
|
20
|
+
details?: LookupOptionDetail[];
|
|
21
|
+
/** Optional data associated with the option */
|
|
22
|
+
data?: unknown;
|
|
23
|
+
/** Whether the option is disabled */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface LookupProps extends Omit<InputProps, 'onChange' | 'value'> {
|
|
27
|
+
/** The options to display in the dropdown */
|
|
28
|
+
options?: LookupOption[];
|
|
29
|
+
/** Currently selected option key (use with options array lookup) */
|
|
30
|
+
selectedKey?: string | null;
|
|
31
|
+
/** Currently selected option (use for controlled selection with full data) */
|
|
32
|
+
selectedOption?: LookupOption | null;
|
|
33
|
+
/** Callback when selection changes - receives full option with data */
|
|
34
|
+
onOptionSelect?: (option: LookupOption | null) => void;
|
|
35
|
+
/** Callback when search text changes - use for async loading */
|
|
36
|
+
onSearchChange?: (searchText: string) => void;
|
|
37
|
+
/** Placeholder text when no selection */
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
/** Whether the lookup is loading options */
|
|
40
|
+
loading?: boolean;
|
|
41
|
+
/** Message to display when no results found */
|
|
42
|
+
noResultsMessage?: string;
|
|
43
|
+
/** Whether to allow clearing the selection */
|
|
44
|
+
clearable?: boolean;
|
|
45
|
+
/** Minimum characters before triggering search */
|
|
46
|
+
minSearchLength?: number;
|
|
47
|
+
/** Debounce delay for search in milliseconds */
|
|
48
|
+
searchDebounceMs?: number;
|
|
49
|
+
/** Whether the dropdown should match the input width */
|
|
50
|
+
matchInputWidth?: boolean;
|
|
51
|
+
/** Header content rendered at the top of the dropdown */
|
|
52
|
+
header?: React.ReactNode;
|
|
53
|
+
/** Footer content rendered at the bottom of the dropdown */
|
|
54
|
+
footer?: React.ReactNode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare const Lookup: React.FC<LookupProps>;
|
|
58
|
+
|
|
59
|
+
export { Lookup, type LookupOption, type LookupOptionDetail, type LookupProps };
|