@apliteni/apliteni-ui 0.33.1 → 0.34.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/package.json +1 -1
- package/react/README.md +87 -1
- package/react/dist/index.d.ts +122 -2
- package/react/dist/index.js +442 -11
- package/src/components/dropdown.js +36 -15
- package/src/components/shell.js +124 -37
- package/src/components/tooltip.js +3 -0
- package/src/styles/back.css +12 -1
- package/src/styles/dropdown.css +37 -10
- package/src/styles/layout.css +107 -6
- package/src/styles/topbar.css +1 -1
package/package.json
CHANGED
package/react/README.md
CHANGED
|
@@ -28,7 +28,7 @@ import '@apliteni/apliteni-ui/react/css'; // React components' shell styles (mo
|
|
|
28
28
|
import { DataTable, Modal, Button } from '@apliteni/apliteni-ui/react';
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Components: `DataTable`, `Pagination`, `StatBand`, `Modal`, `Drawer`, `CommandPalette`, `Button`, `Badge`, `Card`, `Icon`.
|
|
31
|
+
Components: `DataTable`, `Pagination`, `StatBand`, `Modal`, `Drawer`, `CommandPalette`, `Dropdown`, `BackLink`, `Button`, `Badge`, `Card`, `Icon`.
|
|
32
32
|
|
|
33
33
|
`CommandPalette` renders the kit's `commandPalette()` markup, class for class, and imports the
|
|
34
34
|
kit's ranking rather than repeating it — so a palette a server rendered and the same palette
|
|
@@ -94,6 +94,92 @@ closes both. The vanilla `drawer()`, `confirm()` and `commandPalette()` keep a s
|
|
|
94
94
|
stack that this one cannot see, so do not open a React dialog and a vanilla overlay over
|
|
95
95
|
each other on the same page.
|
|
96
96
|
|
|
97
|
+
## Dropdown
|
|
98
|
+
|
|
99
|
+
`Dropdown` renders the kit's `dropdown()` markup, class for class, and carries
|
|
100
|
+
`wireDropdown()`'s keyboard: the arrows open it onto the first row or the selected one and
|
|
101
|
+
then walk the ring, stepping over a disabled row and wrapping at both ends; Home and End go
|
|
102
|
+
to the first and last; Enter and Space pick the row focus is on; Escape closes and returns
|
|
103
|
+
focus to the trigger; Tab closes; a click outside closes it, and opening one closes every
|
|
104
|
+
other on the page. Both flavours are here — `variant="select"` is a listbox that writes the
|
|
105
|
+
pick into the trigger and moves the tick, `variant="menu"` is an action list — and, as in
|
|
106
|
+
the factory, the variant is inferred when you leave it out.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
<Dropdown items={items} ariaLabel="Row actions" triggerContent="Actions"
|
|
110
|
+
onSelect={(value, item) => run(item)} />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**A row can be anything, including a router link.** `row` is handed the item and every prop
|
|
114
|
+
the row has to carry — the classes, the role, the tab stop the panel moves itself, the pick
|
|
115
|
+
and the click — and you spread them onto whatever element the row should be:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { Link } from 'react-router';
|
|
119
|
+
|
|
120
|
+
<Dropdown items={items} row={(item, props) => <Link to={item.href!} {...props} />} />
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
That is the shape #304 was opened for, and it is a render prop rather than an `as` because
|
|
124
|
+
the row is where a router link needs props of its own: `as={Link}` could pass the item's
|
|
125
|
+
`href`, but not the `to`, `state`, `replace` or `prefetch` a real call site writes — and it
|
|
126
|
+
could not vary them per row. Every row is still in the arrow-key ring, because the panel
|
|
127
|
+
finds its rows by `data-dd-item`, which is in the props you spread.
|
|
128
|
+
|
|
129
|
+
`open` and `onOpenChange` make the open state controlled, the way `sort` does on `DataTable`;
|
|
130
|
+
leave `open` out and the component keeps its own, starting from `defaultOpen`. `onSelect`
|
|
131
|
+
reports the item's `value` and the item. Pass `ariaLabel` to a `select` dropdown: a listbox
|
|
132
|
+
needs a name, and axe says so — the same reason every vanilla dropdown story passes one.
|
|
133
|
+
|
|
134
|
+
**`search` puts a field over the rows**, the same one the factory draws, and Guidelines /
|
|
135
|
+
Component choice makes it a rule at ten options or any list fed by data:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
<Dropdown variant="select" label="currency:" ariaLabel="Currency" scroll={220}
|
|
139
|
+
search={{ placeholder: 'Search currencies' }} items={currencies} onSelect={setCurrency} />
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Typing filters and focus stays in the field, which is a `role="combobox"` naming the row Enter
|
|
143
|
+
would pick through `aria-activedescendant`. ↑ and ↓ walk the rows still showing, skip a disabled
|
|
144
|
+
one and wrap; Enter picks; Home and End move the caret, because they belong to the text field;
|
|
145
|
+
Escape closes and Tab closes. Every open starts from the whole list. A query that matches nothing
|
|
146
|
+
shows "No match for “…”" and a nudge — reword either with `search.empty` (where `{q}` stands for
|
|
147
|
+
the query) and `search.hint`. With a field in it the panel is a `role="dialog"`, so every row
|
|
148
|
+
becomes an option: a row carrying `href` is drawn as a plain option rather than a link, exactly as
|
|
149
|
+
the factory draws it — which is what `row` is for when the rows must be router links.
|
|
150
|
+
|
|
151
|
+
**The match is the kit's, not this package's.** `dropdownMatch()` and `dropdownFiltering()` are
|
|
152
|
+
exported from `@apliteni/apliteni-ui` and imported here, the way `CommandPalette` imports
|
|
153
|
+
`rankGroups()`, so a list a server rendered and the same list after a keystroke hide the same
|
|
154
|
+
rows. `Dropdown.test.tsx` types the same query into the factory-plus-`wireDropdown()` and into
|
|
155
|
+
this component and compares what is left.
|
|
156
|
+
|
|
157
|
+
Two things the factory has that this does not, both deliberate. It emits no `data-dropdown`
|
|
158
|
+
on the container, so a page that calls `wireDropdown(document)` cannot adopt a dropdown React
|
|
159
|
+
owns — the same decision `Drawer` makes about `data-drawer`; the row and panel hooks stay,
|
|
160
|
+
because they are the row contract `docs/library.md` publishes. And it has no `portal: true`: the
|
|
161
|
+
panel is a child of the trigger's container, so a dropdown inside `.ui-app__rail`
|
|
162
|
+
(`position: sticky` with `overflow-y: auto`) still wants the vanilla factory.
|
|
163
|
+
|
|
164
|
+
## BackLink
|
|
165
|
+
|
|
166
|
+
`BackLink` is the kit's `backLink()`, rendered by React rather than through
|
|
167
|
+
`dangerouslySetInnerHTML`. It holds the same rules: no address, or a `javascript:` one,
|
|
168
|
+
renders nothing; the label is the destination as the sidebar spells it, and one that already
|
|
169
|
+
says "Back to" is not said twice; the arrow is `aria-hidden`, so the accessible name says
|
|
170
|
+
"Back to" that destination in words.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<BackLink href="/invoices?status=open&page=3" label="Invoices" />
|
|
174
|
+
<BackLink as={Link} to="/invoices" href="/invoices" label="Invoices" />
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
It renders the anchor itself, so `ui-back` is on the element the shell's
|
|
178
|
+
`.ui-app__main > .ui-back` rule looks for — which is the whole reason a stateless component
|
|
179
|
+
is here. `as` takes the element the link is drawn as and passes it every prop this component
|
|
180
|
+
does not read, which is how a router link gets its own `to`; `href` is still required,
|
|
181
|
+
because it is what the `javascript:` guard reads.
|
|
182
|
+
|
|
97
183
|
## Work on them
|
|
98
184
|
|
|
99
185
|
From the repo root — one `npm install` covers the workspace:
|
package/react/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, ButtonHTMLAttributes } from 'react';
|
|
2
|
+
import { ReactNode, ButtonHTMLAttributes, MouseEvent, ElementType, ComponentPropsWithoutRef } from 'react';
|
|
3
3
|
|
|
4
4
|
declare function Icon({ name }: {
|
|
5
5
|
name: string;
|
|
@@ -243,7 +243,127 @@ type DeniedProps = {
|
|
|
243
243
|
};
|
|
244
244
|
declare function Denied({ title, sub, need, icon, className, children, }: DeniedProps): react.JSX.Element;
|
|
245
245
|
|
|
246
|
+
type DropdownBadge = string | {
|
|
247
|
+
text: string;
|
|
248
|
+
tone?: string;
|
|
249
|
+
};
|
|
250
|
+
type DropdownItem = {
|
|
251
|
+
label: string;
|
|
252
|
+
/** The pick, as `onSelect` reports it and as `data-value` carries it. */
|
|
253
|
+
value?: string | number;
|
|
254
|
+
description?: string;
|
|
255
|
+
/** A kit glyph name, drawn in the row's leading slot. */
|
|
256
|
+
icon?: string;
|
|
257
|
+
badge?: DropdownBadge;
|
|
258
|
+
selected?: boolean;
|
|
259
|
+
disabled?: boolean;
|
|
260
|
+
danger?: boolean;
|
|
261
|
+
/** A row that goes somewhere. A `select` row never becomes a link, as the factory decides it. */
|
|
262
|
+
href?: string;
|
|
263
|
+
target?: string;
|
|
264
|
+
};
|
|
265
|
+
/** A rule between rows. `'---'` is the factory's shorthand for the same thing. */
|
|
266
|
+
type DropdownSeparator = '---' | {
|
|
267
|
+
separator: true;
|
|
268
|
+
};
|
|
269
|
+
type DropdownEntry = DropdownItem | DropdownSeparator;
|
|
270
|
+
type DropdownSection = {
|
|
271
|
+
label?: string;
|
|
272
|
+
items?: DropdownEntry[];
|
|
273
|
+
};
|
|
274
|
+
/** The field above the rows. `true` takes every default. */
|
|
275
|
+
type DropdownSearch = {
|
|
276
|
+
placeholder?: string;
|
|
277
|
+
/** Names the field. Without one it is named "Search " + the dropdown's name. */
|
|
278
|
+
label?: string;
|
|
279
|
+
/** The no-match line. `{q}` in it stands for the query. */
|
|
280
|
+
empty?: string;
|
|
281
|
+
/** The nudge under it. */
|
|
282
|
+
hint?: string;
|
|
283
|
+
/** What the field holds before it is opened; every open starts from the whole list. */
|
|
284
|
+
query?: string;
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* What a row has to carry to be a row: the classes, the role, the tab stop the
|
|
288
|
+
* panel moves itself, the pick and the click. Spread it onto whatever element the
|
|
289
|
+
* row should be — a router `<Link>`, a `<button>`, anything.
|
|
290
|
+
*/
|
|
291
|
+
type DropdownRowProps = {
|
|
292
|
+
className: string;
|
|
293
|
+
role: 'option' | 'menuitem';
|
|
294
|
+
tabIndex: -1;
|
|
295
|
+
'data-dd-item': string;
|
|
296
|
+
'data-value'?: string;
|
|
297
|
+
'aria-selected'?: boolean;
|
|
298
|
+
'aria-disabled'?: true;
|
|
299
|
+
href?: string;
|
|
300
|
+
target?: string;
|
|
301
|
+
/** Only with `search`: the field names the row Enter would pick. */
|
|
302
|
+
id?: string;
|
|
303
|
+
hidden?: boolean;
|
|
304
|
+
onClick: (e: MouseEvent) => void;
|
|
305
|
+
children: ReactNode;
|
|
306
|
+
};
|
|
307
|
+
type DropdownProps = {
|
|
308
|
+
/** Muted prefix in the trigger, e.g. "version:". */
|
|
309
|
+
label?: string;
|
|
310
|
+
/** What the trigger shows. Given one, the trigger is yours to update. */
|
|
311
|
+
value?: string;
|
|
312
|
+
placeholder?: string;
|
|
313
|
+
/** Inferred from the items when it is left out, exactly as the factory infers it. */
|
|
314
|
+
variant?: 'select' | 'menu';
|
|
315
|
+
items?: DropdownEntry[];
|
|
316
|
+
/** Grouped alternative to `items`. */
|
|
317
|
+
sections?: DropdownSection[];
|
|
318
|
+
header?: ReactNode;
|
|
319
|
+
footer?: ReactNode;
|
|
320
|
+
/** Replaces the trigger's label/value pair. With `ariaLabel`, names the trigger. */
|
|
321
|
+
triggerContent?: ReactNode;
|
|
322
|
+
triggerClass?: string;
|
|
323
|
+
chevron?: boolean;
|
|
324
|
+
/** The edge the panel hugs. */
|
|
325
|
+
align?: 'start' | 'end';
|
|
326
|
+
/** `'up'` opens into the space above the trigger. */
|
|
327
|
+
direction?: 'down' | 'up';
|
|
328
|
+
/** `true`, or a max height in px, to cap the panel and scroll it. */
|
|
329
|
+
scroll?: boolean | number;
|
|
330
|
+
/** A field above the rows that filters them as the reader types. */
|
|
331
|
+
search?: boolean | DropdownSearch;
|
|
332
|
+
ariaLabel?: string;
|
|
333
|
+
id?: string;
|
|
334
|
+
panelClass?: string;
|
|
335
|
+
/** Controlled: the host owns the open state and answers `onOpenChange`. */
|
|
336
|
+
open?: boolean;
|
|
337
|
+
/** Uncontrolled: where it starts. */
|
|
338
|
+
defaultOpen?: boolean;
|
|
339
|
+
onOpenChange?: (open: boolean) => void;
|
|
340
|
+
onSelect?: (value: string | number | undefined, item: DropdownItem) => void;
|
|
341
|
+
/** Draw the row yourself. `props` is everything the row needs; spread it. */
|
|
342
|
+
row?: (item: DropdownItem, props: DropdownRowProps) => ReactNode;
|
|
343
|
+
};
|
|
344
|
+
declare function Dropdown({ label, value, placeholder, variant, items, sections, header, footer, triggerContent, triggerClass, chevron, align, direction, scroll, search, ariaLabel, id, panelClass, open: openProp, defaultOpen, onOpenChange, onSelect, row, }: DropdownProps): react.JSX.Element;
|
|
345
|
+
|
|
346
|
+
type BackLinkOwnProps = {
|
|
347
|
+
/** The address. No address, or a `javascript:` one, and nothing is rendered. */
|
|
348
|
+
href?: string;
|
|
349
|
+
/** The destination, spelled the way the sidebar or the trail spells it. */
|
|
350
|
+
label?: string;
|
|
351
|
+
className?: string;
|
|
352
|
+
};
|
|
353
|
+
type BackLinkProps<T extends ElementType = 'a'> = BackLinkOwnProps & {
|
|
354
|
+
/** The element the anchor is drawn as — a router `<Link>`, say. Default `'a'`. */
|
|
355
|
+
as?: T;
|
|
356
|
+
} & Omit<ComponentPropsWithoutRef<T>, keyof BackLinkOwnProps | 'as'>;
|
|
357
|
+
/**
|
|
358
|
+
* The link a page under another page puts above its title.
|
|
359
|
+
*
|
|
360
|
+
* The arrow says "back" on screen and is aria-hidden, so the link's accessible name
|
|
361
|
+
* says it in words: "Back to Invoices". Given `as`, the props this component does not
|
|
362
|
+
* read are passed through, which is how a router link gets its own `to`.
|
|
363
|
+
*/
|
|
364
|
+
declare function BackLink<T extends ElementType = 'a'>({ href, label, as, className, ...rest }: BackLinkProps<T>): react.JSX.Element | null;
|
|
365
|
+
|
|
246
366
|
declare const PAGE_SIZES: readonly number[];
|
|
247
367
|
declare const DEFAULT_PAGE_SIZE: number;
|
|
248
368
|
|
|
249
|
-
export { Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, Denied, type DeniedProps, Drawer, type DrawerProps, Icon, Modal, type ModalProps, PAGE_SIZES, Pagination, type PaginationProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, type TableSort, sortTableRows };
|
|
369
|
+
export { BackLink, type BackLinkOwnProps, type BackLinkProps, Badge, BusyRegion, type BusyRegionProps, Button, type ButtonProps, Card, type Column, type CommandGroup, type CommandItem, CommandPalette, type CommandPaletteProps, DEFAULT_PAGE_SIZE, DataTable, type DataTableProps, Denied, type DeniedProps, Drawer, type DrawerProps, Dropdown, type DropdownBadge, type DropdownEntry, type DropdownItem, type DropdownProps, type DropdownRowProps, type DropdownSearch, type DropdownSection, type DropdownSeparator, Icon, Modal, type ModalProps, PAGE_SIZES, Pagination, type PaginationProps, Skeleton, type SkeletonProps, SkeletonTable, type SkeletonTableProps, StatBand, type StatBandProps, type StatDelta, type StatFigure, type StatTone, type StatVariant, type TableSort, sortTableRows };
|