@mendylanda/ui 0.3.0-alpha.7 → 0.3.0-alpha.8
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 +2 -2
- package/dist/filters/filter-bar.d.ts +1 -1
- package/dist/filters/filter-bar.js +38 -6
- package/dist/filters/filter-menu-focus.d.ts +1 -1
- package/dist/filters/filter-menu-panel.d.ts +1 -1
- package/dist/filters/use-menu-placement.d.ts +1 -1
- package/dist/table/table-view.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Opening the menu shows the category list without choosing a filter. Focusing a c
|
|
|
82
82
|
|
|
83
83
|
### Defaults and clearing
|
|
84
84
|
|
|
85
|
-
`FilterBar` and composed `FilterList`
|
|
85
|
+
`FilterBar` and composed `FilterList` show Clear all after the chips when a removable filter is active. It clears filters and search. Search-only bars use the input’s clear control and hide the filter menu when no menu fields are available. Set `showClear={false}` to opt out. Choice search is enabled by default; use `searchable: false` for short lists. Menu selections keep the menu open, and selecting the current single choice again clears it. Chip editor popups open instantly; `editorAnimation` opts into their animation.
|
|
86
86
|
|
|
87
87
|
See [defaults and app configuration](https://ui.mendylanda.com/docs/defaults) for the full behavior and [system reference](https://ui.mendylanda.com/docs/system) for query adapters, selected-label loading, grouped fields, and custom editors.
|
|
88
88
|
|
|
@@ -119,7 +119,7 @@ Pass a filter controller once to place the shared filter bar and connect table r
|
|
|
119
119
|
|
|
120
120
|
Create the controller with `useFilters`, `useUrlFilters`, or `useControlledFilters`. Its values remain application query state. The table does not derive a predicate or fetch rows from filter definitions. Set `filterBar={false}` when controls live elsewhere, or use `TableFilters` and `TableView` with the same controller in a composed layout.
|
|
121
121
|
|
|
122
|
-
Tables fit their rows by default, capped at 65dvh. Full-page layouts can set an explicit `height`. Rows remain 44px unless `rowHeight="auto"` opts into measurement for wrapped or editable content. Selected rows receive the standard highlight; `isRowHighlighted` adds application state. `onRowClick`, `rowClassName`, and `renderRowDetail` cover row actions and expandable content.
|
|
122
|
+
Tables fit their rows, headers, and empty content by default, capped at 65dvh. Short tables do not reserve a vertical scrollbar. Full-page layouts can set an explicit `height`. Rows remain 44px unless `rowHeight="auto"` opts into measurement for wrapped or editable content. Selected rows receive the standard highlight; `isRowHighlighted` adds application state. `onRowClick`, `rowClassName`, and `renderRowDetail` cover row actions and expandable content.
|
|
123
123
|
|
|
124
124
|
Changes to table query state or the shared controller reset scroll and cell selection. Appending rows keeps the current position and selection. Empty states distinguish an empty dataset, an empty filtered result with one controller clear, and an empty later page that can return to the first page. Locked filters never get a clear action.
|
|
125
125
|
|
|
@@ -38,7 +38,7 @@ export declare function FilterMenu({ children, asChild, anchor, }: {
|
|
|
38
38
|
asChild?: boolean;
|
|
39
39
|
/** Align a standalone trigger to an application-owned search field. */
|
|
40
40
|
anchor?: React.RefObject<HTMLDivElement | null>;
|
|
41
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
42
42
|
/** Applied chips followed by Clear all. Opt out when placing FilterClear elsewhere. */
|
|
43
43
|
export declare function FilterList({ showClear }: {
|
|
44
44
|
showClear?: boolean;
|
|
@@ -23,6 +23,12 @@ const subscribeHydration = () => () => { };
|
|
|
23
23
|
const clientHydrated = () => true;
|
|
24
24
|
const serverHydrated = () => false;
|
|
25
25
|
const Context = createContext(null);
|
|
26
|
+
function availableMenuEntries(filters) {
|
|
27
|
+
return filters.entries.filter((entry) => !entry.field.hidden && entry.field.menu !== false);
|
|
28
|
+
}
|
|
29
|
+
function hasClearableActiveField(filters) {
|
|
30
|
+
return filters.entries.some(({ field, value }) => !field.hidden && !field.disabled && field.removable !== false && field.isActive(value));
|
|
31
|
+
}
|
|
26
32
|
function useRoot() {
|
|
27
33
|
const context = useContext(Context);
|
|
28
34
|
if (!context)
|
|
@@ -69,7 +75,22 @@ export function FilterSearch({ label = "Search", placeholder = "Search or filter
|
|
|
69
75
|
const anchor = useRef(null);
|
|
70
76
|
const input = useRef(null);
|
|
71
77
|
const shift = useRef(false);
|
|
72
|
-
|
|
78
|
+
const hasMenu = availableMenuEntries(filters).length > 0;
|
|
79
|
+
useLayoutEffect(() => {
|
|
80
|
+
if (hasMenu)
|
|
81
|
+
return;
|
|
82
|
+
const focusTarget = input.current;
|
|
83
|
+
trigger.current = focusTarget;
|
|
84
|
+
return () => {
|
|
85
|
+
if (trigger.current === focusTarget)
|
|
86
|
+
trigger.current = null;
|
|
87
|
+
};
|
|
88
|
+
}, [hasMenu, trigger]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!hasMenu && filters.menuOpen)
|
|
91
|
+
filters.setMenuOpen(false);
|
|
92
|
+
}, [filters, hasMenu]);
|
|
93
|
+
return (_jsxs(DropdownMenu, { dir: direction, modal: false, open: hasMenu && filters.menuOpen, onOpenChange: filters.setMenuOpen, children: [_jsxs("div", { ref: anchor, className: cn("mui-d2d9e1f13413 mui-58d0413dd93c mui-81b198a2db84 mui-27ead27a81df mui-0cc20f4e20c8", classNames?.search), children: [_jsx(Search, { className: "mui-33228ff7d583 mui-747355bdc2a2 mui-cea70f23972e mui-4e45f4841abb mui-100c22d5776b mui-5dde0fd996f0", "aria-hidden": "true" }), _jsx(Input, { ref: input, type: "text", role: "searchbox", disabled: disabled, "aria-label": label, placeholder: placeholder, value: filters.search, autoComplete: "off", autoCapitalize: "none", autoCorrect: "off", spellCheck: false, className: cn("mui-58d0413dd93c mui-b2100041c585 mui-c74ab393b96d mui-4f05a238500e", filters.search ? (hasMenu ? "mui-559069d2c2e5" : "mui-0e14c365f9e8") : hasMenu ? "mui-0e14c365f9e8" : "mui-a202804a1738", classNames?.searchInput), onKeyDown: (event) => {
|
|
73
94
|
shift.current = event.shiftKey;
|
|
74
95
|
if (event.key === "Enter" && !event.nativeEvent.isComposing && !event.shiftKey) {
|
|
75
96
|
const classified = classifyPaste(event.currentTarget.value, filters.entries);
|
|
@@ -105,17 +126,28 @@ export function FilterSearch({ label = "Search", placeholder = "Search or filter
|
|
|
105
126
|
after: element.value.slice(end),
|
|
106
127
|
});
|
|
107
128
|
setAmbiguous(result.ambiguous);
|
|
108
|
-
} }), filters.search && (_jsx(Button, { variant: "ghost", size: "icon", type: "button", disabled: disabled, "aria-label": "Clear search", className: "mui-747355bdc2a2 mui-
|
|
129
|
+
} }), filters.search && (_jsx(Button, { variant: "ghost", size: "icon", type: "button", disabled: disabled, "aria-label": "Clear search", className: cn("mui-747355bdc2a2 mui-4e45f4841abb mui-222f930b8752 mui-0712570dcc9f mui-5dde0fd996f0 mui-71556df3b421 mui-a503dd374cca mui-02e603944040 mui-0b5a4d1257ab mui-8da02faac748 mui-2e357972fc10 mui-39be5a15b766 mui-db549d38ff19", hasMenu ? "mui-585f4729f611" : "mui-13422ddc8d83"), onClick: () => {
|
|
109
130
|
setAmbiguous([]);
|
|
110
131
|
filters.setSearch("");
|
|
111
132
|
input.current?.focus();
|
|
112
|
-
}, children: _jsx(X, { className: "mui-61c000c8a98a", "aria-hidden": "true" }) })), _jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", ref:
|
|
133
|
+
}, children: _jsx(X, { className: "mui-61c000c8a98a", "aria-hidden": "true" }) })), hasMenu && (_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", ref: (element) => {
|
|
134
|
+
trigger.current = element;
|
|
135
|
+
}, type: "button", disabled: disabled, "aria-label": "Open filters", "aria-haspopup": "dialog", className: cn("mui-747355bdc2a2 mui-13422ddc8d83 mui-4e45f4841abb mui-79ede756e761 mui-5dde0fd996f0 mui-2cdd01663c76 mui-8da02faac748 mui-2e357972fc10 mui-39be5a15b766 mui-db549d38ff19 mui-790ed041a953", filters.active.length ? "mui-65694092f002" : "mui-0b5a4d1257ab", classNames?.menuTrigger), children: _jsx(ListFilter, { className: "mui-100c22d5776b", "aria-hidden": "true" }) }) }))] }), hasMenu && filters.menuOpen && _jsx(FilterMenuContent, { anchor: anchor })] }));
|
|
113
136
|
}
|
|
114
137
|
/** A standalone menu button for layouts without a search field. */
|
|
115
138
|
export function FilterMenu({ children = "Add filter", asChild = false, anchor, }) {
|
|
116
139
|
const { filters, trigger, disabled, direction } = useRoot();
|
|
117
140
|
const { classNames } = useMendyUI();
|
|
118
|
-
|
|
141
|
+
const hasMenu = availableMenuEntries(filters).length > 0;
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!hasMenu && filters.menuOpen)
|
|
144
|
+
filters.setMenuOpen(false);
|
|
145
|
+
}, [filters, hasMenu]);
|
|
146
|
+
if (!hasMenu)
|
|
147
|
+
return null;
|
|
148
|
+
return (_jsxs(DropdownMenu, { dir: direction, modal: false, open: filters.menuOpen, onOpenChange: filters.setMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { ref: (element) => {
|
|
149
|
+
trigger.current = element;
|
|
150
|
+
}, asChild: asChild, className: classNames?.menuTrigger, variant: "outline", disabled: disabled, "aria-haspopup": "dialog", children: children }) }), filters.menuOpen && _jsx(FilterMenuContent, { anchor: anchor })] }));
|
|
119
151
|
}
|
|
120
152
|
const fieldIcons = {
|
|
121
153
|
single: Circle,
|
|
@@ -131,7 +163,7 @@ function FilterMenuContent({ anchor }) {
|
|
|
131
163
|
const [clearEpoch, setClearEpoch] = useState(0);
|
|
132
164
|
const [drafts] = useState(() => new Map());
|
|
133
165
|
const grouped = new Set(groups.flatMap((group) => group.fields));
|
|
134
|
-
const visible = filters
|
|
166
|
+
const visible = availableMenuEntries(filters);
|
|
135
167
|
const sections = [
|
|
136
168
|
...visible.flatMap((entry) => grouped.has(entry.id)
|
|
137
169
|
? []
|
|
@@ -190,7 +222,7 @@ export function FilterList({ showClear = true }) {
|
|
|
190
222
|
(entry.field.isActive(entry.value) ||
|
|
191
223
|
showSuggestion(entry, suggestions, filters.active.length))
|
|
192
224
|
? [_jsx(FieldChip, { entry: entry }, entry.id)]
|
|
193
|
-
: []), showClear && _jsx(FilterClear, {})] }));
|
|
225
|
+
: []), showClear && hasClearableActiveField(filters) && _jsx(FilterClear, {})] }));
|
|
194
226
|
}
|
|
195
227
|
function summarize(field, value, choices) {
|
|
196
228
|
if (field.kind === "numberRange" && Array.isArray(value))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { KeyboardEvent } from "react";
|
|
2
2
|
export declare function focusMenuEditor(root: HTMLDivElement | null): void;
|
|
3
3
|
export declare function handleMenuTab(event: KeyboardEvent<HTMLDivElement>, { trigger, editor, onClose, hasSelection, }: {
|
|
4
|
-
trigger:
|
|
4
|
+
trigger: HTMLElement | null;
|
|
5
5
|
editor: HTMLDivElement | null;
|
|
6
6
|
onClose(): void;
|
|
7
7
|
hasSelection: boolean;
|
|
@@ -16,7 +16,7 @@ interface FilterMenuPanelProps {
|
|
|
16
16
|
onSelect(id: string | null): void;
|
|
17
17
|
onClose(): void;
|
|
18
18
|
anchor?: RefObject<HTMLDivElement | null>;
|
|
19
|
-
trigger: RefObject<
|
|
19
|
+
trigger: RefObject<HTMLElement | null>;
|
|
20
20
|
}
|
|
21
21
|
/** One dialog contains the filter list and its editor, with a single-panel layout on phones. */
|
|
22
22
|
export declare function FilterMenuPanel({ sections, selectedId, onSelect, onClose, anchor, trigger, }: FilterMenuPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CSSProperties, RefObject } from "react";
|
|
2
2
|
/** Position the menu against the search field and the available viewport. */
|
|
3
|
-
export declare function useMenuPlacement(anchor: RefObject<HTMLDivElement | null> | undefined, trigger: RefObject<
|
|
3
|
+
export declare function useMenuPlacement(anchor: RefObject<HTMLDivElement | null> | undefined, trigger: RefObject<HTMLElement | null>, desktop: boolean): {
|
|
4
4
|
alignOffset: number;
|
|
5
5
|
side: "top" | "bottom";
|
|
6
6
|
anchorStyle: CSSProperties | undefined;
|
package/dist/table/table-view.js
CHANGED
|
@@ -98,10 +98,11 @@ export function TableView({ table, label = "Data table", height = "auto", rowHei
|
|
|
98
98
|
]);
|
|
99
99
|
const headersById = new Map(table.getFlatHeaders().map((header) => [header.column.id, header]));
|
|
100
100
|
return (_jsxs("div", { "data-mendy-ui": "", className: "mui-184ddc11e5f9", children: [_jsx("div", { "aria-live": "polite", className: "mui-32fb090591d9", children: announcement }), _jsxs("div", { ref: setContainer, role: "grid", tabIndex: -1, "aria-label": label, "aria-rowcount": renderRowDetail ? -1 : rows.length + 1, "aria-colcount": layout.columns.length, "aria-busy": status === "loading" || refreshing, style: {
|
|
101
|
-
height: height === "auto"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
height: height === "auto" ? undefined : height,
|
|
102
|
+
maxHeight: height === "auto" ? "65dvh" : undefined,
|
|
103
|
+
}, className: cn("mui-d2d9e1f13413 mui-014aadadffad mui-1a26a0d28420 mui-3fa8c572949b mui-4f1a55de40bc mui-5b272f3c5076 mui-c74ab393b96d mui-b30fc56058b6", className), onKeyDown: onKeyDown, children: [_jsx("div", { role: "row", "aria-rowindex": 1, className: "mui-964a9431ff49 mui-98599e4ee250 mui-4e8f0a87a0dc mui-222f930b8752 mui-2bf6510f1330 mui-bbe39cfb5cc6 mui-5b272f3c5076", style: { width: totalWidth }, children: columns.map((column) => (_jsxs(Fragment, { children: [columnGaps.has(column.id) && (_jsx("div", { "aria-hidden": "true", className: "mui-27ead27a81df", style: { width: columnGaps.get(column.id) } })), _jsx(TableHeaderCell, { table: table, header: headersById.get(column.id), index: columnIndexes.get(column.id), style: cellStyle(column), renderHeader: renderHeader, contentClassName: contentClassName }, column.id)] }, column.id))) }), _jsx(TableInitialState, { status: status, hasRows: rows.length > 0, loadingState: loadingState ?? (_jsx(TableLoadingRows, { columns: columns, columnGaps: columnGaps, width: totalWidth, rowHeight: rowHeight, cellStyle: cellStyle, count: height === "auto"
|
|
104
|
+
? 8
|
|
105
|
+
: Math.max(1, Math.ceil(((virtual.scrollRect?.height ?? 400) - rowHeight) / rowHeight)) })), emptyState: emptyState, hasFilters: hasFilters, pageIndex: table.state.pagination.pageIndex, clearFilters: clearFilters, firstPage: () => table.setPageIndex(0), error: error, retry: retry }), _jsx("div", { role: "rowgroup", className: "mui-d2d9e1f13413 mui-2bf6510f1330", style: { height: virtual.getTotalSize(), width: totalWidth }, children: items.map((item) => (_jsx(TableBodyRow, { row: rows[item.index], rowIndex: item.index, start: item.start, rowHeight: rowHeight, autoRowHeight: autoRowHeight, measureElement: autoRowHeight ? virtual.measureElement : undefined, rowClassName: rowClassName, renderRowDetail: renderRowDetail, detailWidth: viewportWidth ?? totalWidth, cellStyle: cellStyle, columns: columns, columnGaps: columnGaps, columnIndexes: columnIndexes, contentVersion: table.options, contentState: table.state, focusedId: focused?.id, copied: copied, contentClassName: contentClassName, onRowClick: onRowClick, onRowActivate: onRowActivate, isRowHighlighted: isRowHighlighted }, item.key))) }), rows.length > 0 && status === "error" && (_jsxs("div", { role: "alert", className: "mui-964a9431ff49 mui-30150dd033ab mui-5b272f3c5076 mui-094f5333853b", children: [error ?? "Could not refresh rows.", retry && (_jsx(Button, { onClick: retry, variant: "outline", size: "sm", children: "Retry" }))] })), loadMore?.available && (_jsxs("div", { className: "mui-964a9431ff49 mui-635702706586 mui-222f930b8752 mui-71556df3b421 mui-a503dd374cca mui-074569488cca mui-96b92a971c95 mui-c74ab393b96d mui-35f35c41d134", children: [loadMore.loading ? (_jsxs("div", { role: "status", className: "mui-222f930b8752 mui-71556df3b421 mui-074569488cca", children: [_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "mui-2bd43fb57913 mui-27ead27a81df mui-2c14b55d77e6 mui-237747164b60", children: _jsx("path", { d: "M12 3v3m6.366-.366-2.12 2.12M21 12h-3m.366 6.366-2.12-2.12M12 21v-3m-6.366.366 2.12-2.12M3 12h3m-.366-6.366 2.12 2.12" }) }), _jsx("span", { children: "Loading more\u2026" })] })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
|
|
105
106
|
requested.current = null;
|
|
106
107
|
void Promise.resolve()
|
|
107
108
|
.then(() => loadMore.load())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mendylanda/ui",
|
|
3
|
-
"version": "0.3.0-alpha.
|
|
3
|
+
"version": "0.3.0-alpha.8",
|
|
4
4
|
"description": "Mendy Landa’s reusable React components. Typed filters and tables with shared interaction behavior and customizable UI.",
|
|
5
5
|
"homepage": "https://ui.mendylanda.com",
|
|
6
6
|
"license": "MIT",
|