@cahyo-dimas/freeday 1.53.0 → 2.0.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/CHANGELOG.md +657 -535
- package/COMPONENTS.md +153 -135
- package/README.id.md +46 -45
- package/README.md +46 -43
- package/USAGE.md +39 -35
- package/adapters/blazor/FdyAppShell.razor +50 -0
- package/adapters/blazor/FdyAppShell.razor.cs +114 -0
- package/adapters/blazor/FdyCfl.razor.cs +1 -1
- package/adapters/blazor/FdyChart.razor +1 -1
- package/adapters/blazor/FdyChart.razor.cs +3 -3
- package/adapters/blazor/FdyChartSeries.cs +1 -1
- package/adapters/blazor/FdyCombo.razor.cs +2 -2
- package/adapters/blazor/FdyModal.razor +1 -1
- package/adapters/blazor/FdyTable.razor.cs +7 -7
- package/adapters/blazor/FdyTableFilter.razor +1 -1
- package/adapters/blazor/FdyTableFooter.razor +1 -1
- package/adapters/blazor/FdyTableFooter.razor.cs +4 -4
- package/adapters/blazor/Freeday.Blazor.csproj +1 -1
- package/adapters/blazor/FreedayComponentBase.cs +1 -1
- package/adapters/blazor/TableModel.cs +3 -3
- package/adapters/blazor/TableTypes.cs +3 -3
- package/adapters/blazor/freeday-blazor.js +5 -5
- package/adapters/core/app-shell.d.ts +30 -0
- package/adapters/core/app-shell.js +134 -0
- package/adapters/core/table-model.d.ts +2 -2
- package/adapters/core/table-model.js +8 -8
- package/adapters/react/components/FdyAppShell.tsx +159 -0
- package/adapters/react/components/FdyAutocomplete.tsx +1 -1
- package/adapters/react/components/FdyCascade.tsx +1 -1
- package/adapters/react/components/FdyCfl.tsx +11 -11
- package/adapters/react/components/FdyDateRange.tsx +2 -2
- package/adapters/react/components/FdyDatepicker.tsx +5 -5
- package/adapters/react/components/FdyDrawer.tsx +1 -1
- package/adapters/react/components/FdyModal.tsx +2 -2
- package/adapters/react/components/FdyTable.tsx +7 -7
- package/adapters/react/components/FdyTableFilter.tsx +1 -1
- package/adapters/react/components/FdyTableFooter.tsx +5 -5
- package/adapters/react/index.d.ts +2 -1
- package/adapters/react/index.js +1 -0
- package/adapters/react/useFreeday.js +3 -3
- package/adapters/vue/components/FdyAppShell.vue +160 -0
- package/adapters/vue/components/FdyAutocomplete.vue +1 -1
- package/adapters/vue/components/FdyCascade.vue +1 -1
- package/adapters/vue/components/FdyCfl.vue +8 -8
- package/adapters/vue/components/FdyChart.vue +2 -2
- package/adapters/vue/components/FdyCombo.vue +1 -1
- package/adapters/vue/components/FdyDateRange.vue +2 -2
- package/adapters/vue/components/FdyDatepicker.vue +5 -5
- package/adapters/vue/components/FdyDrawer.vue +3 -3
- package/adapters/vue/components/FdyModal.vue +3 -3
- package/adapters/vue/components/FdyTable.vue +7 -7
- package/adapters/vue/components/FdyTableFilter.vue +1 -1
- package/adapters/vue/components/FdyTableFooter.vue +5 -5
- package/adapters/vue/index.d.ts +3 -2
- package/adapters/vue/index.js +1 -0
- package/adapters/vue/useFreeday.js +3 -3
- package/dist/asset.d.ts +3 -3
- package/dist/freeday-app-shell.js +49 -9
- package/dist/freeday-autocomplete.js +2 -2
- package/dist/freeday-breakpoint.js +1 -1
- package/dist/freeday-carousel.js +5 -5
- package/dist/freeday-cascade.js +7 -7
- package/dist/freeday-cfl.js +9 -9
- package/dist/freeday-chart.js +11 -11
- package/dist/freeday-chip.js +3 -3
- package/dist/freeday-datepicker.js +11 -7
- package/dist/freeday-datetime.js +5 -5
- package/dist/freeday-drawer.js +1 -1
- package/dist/freeday-form.js +14 -14
- package/dist/freeday-mask.js +6 -6
- package/dist/freeday-menu.js +1 -1
- package/dist/freeday-number.js +3 -3
- package/dist/freeday-popover.js +3 -3
- package/dist/freeday-rating.js +2 -2
- package/dist/freeday-select.js +5 -5
- package/dist/freeday-slider.js +2 -2
- package/dist/freeday-stepper.js +7 -7
- package/dist/freeday-table.js +15 -15
- package/dist/freeday-tabs.js +1 -1
- package/dist/freeday-timepicker.js +2 -2
- package/dist/freeday-toast.js +8 -8
- package/dist/freeday-tree.js +3 -3
- package/dist/freeday-upload.js +25 -25
- package/dist/freeday.bundle.css +176 -176
- package/dist/freeday.css +170 -170
- package/dist/freeday.d.ts +2 -2
- package/dist/freeday.js +202 -158
- package/dist/freeday.tokens.css +5 -5
- package/docs/agent-onboarding.md +42 -42
- package/docs/getting-started.md +35 -35
- package/docs/integrations.md +18 -18
- package/docs/reference-screen.html +14 -14
- package/package.json +5 -4
- package/src/base.css +6 -6
- package/src/components/accordion.css +4 -4
- package/src/components/alert.css +1 -1
- package/src/components/app-shell.css +10 -10
- package/src/components/appbar.css +3 -3
- package/src/components/autocomplete.css +1 -1
- package/src/components/avatar.css +3 -3
- package/src/components/badge.css +5 -5
- package/src/components/breadcrumb.css +1 -1
- package/src/components/breakpoints.css +4 -4
- package/src/components/button.css +13 -13
- package/src/components/card.css +8 -8
- package/src/components/carousel.css +2 -2
- package/src/components/cascade.css +2 -2
- package/src/components/cfl.css +4 -4
- package/src/components/chart.css +3 -3
- package/src/components/chip.css +2 -2
- package/src/components/combo.css +3 -3
- package/src/components/composition.css +7 -7
- package/src/components/datepicker.css +6 -6
- package/src/components/datetimepicker.css +1 -1
- package/src/components/description-list.css +1 -1
- package/src/components/divider.css +1 -1
- package/src/components/drawer.css +1 -1
- package/src/components/file-upload.css +5 -5
- package/src/components/filterbar.css +7 -7
- package/src/components/form-grid.css +2 -2
- package/src/components/input-group.css +6 -6
- package/src/components/input.css +6 -6
- package/src/components/kbd.css +1 -1
- package/src/components/list.css +6 -6
- package/src/components/menu.css +3 -3
- package/src/components/modal.css +1 -1
- package/src/components/pagination.css +1 -1
- package/src/components/progress.css +1 -1
- package/src/components/rating.css +2 -2
- package/src/components/selection.css +4 -4
- package/src/components/skeleton.css +1 -1
- package/src/components/slider.css +1 -1
- package/src/components/spinner.css +1 -1
- package/src/components/states.css +1 -1
- package/src/components/stepper.css +2 -2
- package/src/components/table.css +15 -15
- package/src/components/tabs.css +3 -3
- package/src/components/timeline.css +1 -1
- package/src/components/timepicker.css +1 -1
- package/src/components/toast.css +3 -3
- package/src/components/tooltip.css +1 -1
- package/src/components/tree.css +2 -2
- package/tokens/breakpoints.d.ts +2 -2
- package/tokens/breakpoints.mjs +3 -3
- package/tokens/tokens.json +1 -1
|
@@ -3,7 +3,7 @@ import { useEffect, useId, useRef } from 'react';
|
|
|
3
3
|
|
|
4
4
|
// A controlled React wrapper over freeday's `.fdy-modal` native <dialog> (src/components/modal.css).
|
|
5
5
|
// React port of adapters/vue/components/FdyModal.vue: reconciles the reactive `open` boolean with a
|
|
6
|
-
// DOM element whose open/close is a method call
|
|
6
|
+
// DOM element whose open/close is a method call, showModal()/close() guarded against the
|
|
7
7
|
// already-open/closed cases (showModal() on an open dialog throws), onCancel + preventDefault so Esc
|
|
8
8
|
// routes through app state instead of closing the DOM behind its back, and backdrop-click detection
|
|
9
9
|
// via `event.target === dialogEl`. Native <dialog> already gives the focus trap, focus restore,
|
|
@@ -44,7 +44,7 @@ export function FdyModal(props: FdyModalProps): JSX.Element {
|
|
|
44
44
|
if (dismissible) props.onClose();
|
|
45
45
|
}}
|
|
46
46
|
onClick={(e): void => {
|
|
47
|
-
// The ::backdrop is not a separate element
|
|
47
|
+
// The ::backdrop is not a separate element, target === the dialog box means a backdrop click.
|
|
48
48
|
if (dismissible && e.target === dialogRef.current) props.onClose();
|
|
49
49
|
}}
|
|
50
50
|
>
|
|
@@ -45,14 +45,14 @@ export interface FdyTableProps<Row extends object> {
|
|
|
45
45
|
/** Client-side page size when `page` is absent; 0/undefined = render all rows (no pager). */
|
|
46
46
|
pageSize?: number;
|
|
47
47
|
/**
|
|
48
|
-
* Controlled client-side page index (0-based). Provide it
|
|
48
|
+
* Controlled client-side page index (0-based). Provide it, with `pageSize`, without `page`, to
|
|
49
49
|
* own the page while the table keeps doing filter/sort/paginate. This is what lets an EXTERNAL
|
|
50
50
|
* pager drive the table: a responsive screen that hides `.fdy-datatable` below `md` and renders a
|
|
51
51
|
* card list from `onProcess` can render one pager for both breakpoints and point it here.
|
|
52
52
|
* Omit for the internal index (unchanged default).
|
|
53
53
|
*/
|
|
54
54
|
/** Withhold the table's own footer (pager + range) so the screen can render one. Server mode had
|
|
55
|
-
* no way to do this: the app owns the page there ANYWAY, and was still handed a second control
|
|
55
|
+
* no way to do this: the app owns the page there ANYWAY, and was still handed a second control,
|
|
56
56
|
* a responsive list that shows a table at one breakpoint and cards at another ended up with the
|
|
57
57
|
* kit's pager stacked under its own. Client mode's counterpart is `pageIndex`. Default true. */
|
|
58
58
|
pager?: boolean;
|
|
@@ -61,13 +61,13 @@ export interface FdyTableProps<Row extends object> {
|
|
|
61
61
|
* (unchanged default). Every back office has one, and a table that renders two thirds of its own
|
|
62
62
|
* footer forces the app to rebuild all three to add the last (#008).
|
|
63
63
|
*
|
|
64
|
-
* Server mode reports the pick through `onPageChange
|
|
64
|
+
* Server mode reports the pick through `onPageChange`, same callback as a page click, with a new
|
|
65
65
|
* `size`. Client mode keeps it internally and also calls `onPageSizeChange`, so the control works
|
|
66
66
|
* with nothing wired and can still be persisted.
|
|
67
67
|
*/
|
|
68
68
|
pageSizes?: readonly number[];
|
|
69
69
|
/** Client mode with `pageSizes`: the reader picked a new rows-per-page. The table has already
|
|
70
|
-
* applied it
|
|
70
|
+
* applied it, this is for a caller that wants to persist the choice. */
|
|
71
71
|
onPageSizeChange?: (size: number) => void;
|
|
72
72
|
pageIndex?: number;
|
|
73
73
|
/** Client mode with `pageIndex` provided: the table asks for a new 0-based index (pager click, a
|
|
@@ -86,7 +86,7 @@ export interface FdyTableProps<Row extends object> {
|
|
|
86
86
|
rowClass?: (row: Row) => string | undefined;
|
|
87
87
|
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
88
88
|
onRowActivate?: (row: Row) => void;
|
|
89
|
-
/** Called with the processed page of rows (after filter/sort/paginate) plus the total row count
|
|
89
|
+
/** Called with the processed page of rows (after filter/sort/paginate) plus the total row count,
|
|
90
90
|
* in BOTH modes, whenever they change. Lets a consumer render the SAME processed set elsewhere
|
|
91
91
|
* (a `< md` card list, a "selected" summary, export-to-CSV) without re-deriving the pipeline. */
|
|
92
92
|
onProcess?: (result: { rows: Row[]; total: number }) => void;
|
|
@@ -105,7 +105,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
105
105
|
const [internalFilters, setInternalFilters] = useState<FdyFilterMap>({});
|
|
106
106
|
const [internalPageIndex, setInternalPageIndex] = useState<number>(0);
|
|
107
107
|
/* Client-mode rows-per-page. `pageSize` is a plain prop with no callback, so a footer control that
|
|
108
|
-
* only reported would do nothing in the app that wired nothing
|
|
108
|
+
* only reported would do nothing in the app that wired nothing, the table applies the pick itself
|
|
109
109
|
* and reports it. `prevPageSize` is React's adjust-state-during-render idiom: an explicit change to
|
|
110
110
|
* the prop wins back, because the parent said something newer. */
|
|
111
111
|
const [internalPageSize, setInternalPageSize] = useState<number | null>(null);
|
|
@@ -238,7 +238,7 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
|
|
|
238
238
|
.join(' ');
|
|
239
239
|
return cls === '' ? undefined : cls;
|
|
240
240
|
}
|
|
241
|
-
// Enter/Space activate only when the row itself is focused
|
|
241
|
+
// Enter/Space activate only when the row itself is focused, a control inside a cell keeps its own
|
|
242
242
|
// event (the `event.target !== event.currentTarget` guard). Click relies on inner controls calling
|
|
243
243
|
// stopPropagation, matching the pattern consumers hand-roll today.
|
|
244
244
|
function onRowKeydown(e: React.KeyboardEvent<HTMLTableRowElement>, row: Row): void {
|
|
@@ -7,7 +7,7 @@ import type { FdyColumnFilter, FdyColumnFilterType } from '../../core/table-mode
|
|
|
7
7
|
// Internal to FdyTable: one column's header funnel button + its type-aware filter popover
|
|
8
8
|
// (text / enum / number / date) over freeday's `.fdy-table__filterbtn` + `.fdy-filter*` classes.
|
|
9
9
|
// React port of adapters/vue/components/FdyTableFilter.vue. Reuses usePopover so the panel escapes
|
|
10
|
-
// the table's `overflow:hidden` via the top layer. Purely controlled
|
|
10
|
+
// the table's `overflow:hidden` via the top layer. Purely controlled, renders the current
|
|
11
11
|
// `filter`, emits the next one (or null to clear); the parent owns where it goes. Not exported.
|
|
12
12
|
|
|
13
13
|
export interface FdyTableFilterProps {
|
|
@@ -8,19 +8,19 @@ import { FdyCombo } from './FdyCombo';
|
|
|
8
8
|
* The band under a table: what you are looking at, how much of it you see, where you are.
|
|
9
9
|
*
|
|
10
10
|
* `FdyTable` renders this itself, so most screens never import it. It is exported for the one shape
|
|
11
|
-
* that cannot use the table's own
|
|
11
|
+
* that cannot use the table's own, a RESPONSIVE list, where a `.fdy-datatable` at `lg` and a
|
|
12
12
|
* `.fdy-list` below it are two renderings of ONE page of rows. A footer inside the table is inside
|
|
13
13
|
* the half that is hidden on a phone, so those screens need to render it once, outside both
|
|
14
14
|
* (improvement notes #005 and #008, from IDU_EMATE_APPL_WEB).
|
|
15
15
|
*
|
|
16
|
-
* It owns nothing. `page` in, `onPageChange` out
|
|
16
|
+
* It owns nothing. `page` in, `onPageChange` out, the same contract as the table's server mode.
|
|
17
17
|
*/
|
|
18
18
|
export interface FdyTableFooterProps {
|
|
19
19
|
/** The page being shown. `size` drives the range AND the rows-per-page control's value. */
|
|
20
20
|
page: FdyPageState;
|
|
21
21
|
onPageChange?: (page: FdyPageState) => void;
|
|
22
22
|
/**
|
|
23
|
-
* Offer a rows-per-page control. Omit for none
|
|
23
|
+
* Offer a rows-per-page control. Omit for none; the footer is then range + pager, exactly as
|
|
24
24
|
* before. Picking a size calls `onPageChange` with the new `size` and the index that still holds
|
|
25
25
|
* the row you were looking at.
|
|
26
26
|
*/
|
|
@@ -29,7 +29,7 @@ export interface FdyTableFooterProps {
|
|
|
29
29
|
|
|
30
30
|
export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
31
31
|
const { index, size, total } = props.page;
|
|
32
|
-
/* The combo is labelled by the visible word beside it
|
|
32
|
+
/* The combo is labelled by the visible word beside it, a <label htmlFor> cannot reach inside a
|
|
33
33
|
* component, and an aria-label would leave that word attached to nothing. */
|
|
34
34
|
const sizeLabelId: string = useId();
|
|
35
35
|
|
|
@@ -41,7 +41,7 @@ export function FdyTableFooter(props: FdyTableFooterProps): JSX.Element | null {
|
|
|
41
41
|
|
|
42
42
|
const hasPager: boolean = size > 0 && totalPages > 1;
|
|
43
43
|
const sizes: readonly number[] = props.pageSizes ?? [];
|
|
44
|
-
/* One page and no size control means there is nothing here to say
|
|
44
|
+
/* One page and no size control means there is nothing here to say, the table has always withheld
|
|
45
45
|
* the whole band in that case, and this is where that decision now lives. */
|
|
46
46
|
if (!hasPager && sizes.length === 0) return null;
|
|
47
47
|
|
|
@@ -36,11 +36,12 @@ export { FdyCascade, type FdyCascadeProps, type CascadeNode } from './components
|
|
|
36
36
|
export { FdyCfl, type FdyCflProps, type CflColumn, type CflPage } from './components/FdyCfl';
|
|
37
37
|
export { FdyChart, type FdyChartProps, type FdyChartSeries } from './components/FdyChart';
|
|
38
38
|
export { FdyTable, type FdyTableProps } from './components/FdyTable';
|
|
39
|
-
/** The table's own footer, standalone
|
|
39
|
+
/** The table's own footer, standalone, for a responsive list whose table is hidden at some
|
|
40
40
|
* breakpoints and so cannot host it. */
|
|
41
41
|
export { FdyTableFooter, type FdyTableFooterProps } from './components/FdyTableFooter';
|
|
42
42
|
export { FdyModal, type FdyModalProps } from './components/FdyModal';
|
|
43
43
|
export { FdyDrawer, type FdyDrawerProps } from './components/FdyDrawer';
|
|
44
|
+
export { FdyAppShell, type FdyAppShellProps } from './components/FdyAppShell';
|
|
44
45
|
|
|
45
46
|
/** Controlled data-table types (shared, framework-agnostic core). */
|
|
46
47
|
export type {
|
package/adapters/react/index.js
CHANGED
|
@@ -11,3 +11,4 @@ export { FdyTable } from './components/FdyTable.tsx';
|
|
|
11
11
|
export { FdyTableFooter } from './components/FdyTableFooter.tsx';
|
|
12
12
|
export { FdyModal } from './components/FdyModal.tsx';
|
|
13
13
|
export { FdyDrawer } from './components/FdyDrawer.tsx';
|
|
14
|
+
export { FdyAppShell } from './components/FdyAppShell.tsx';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Freeday
|
|
1
|
+
/* Freeday. React adapter (thin). The enhancers stay the source of truth; this
|
|
2
2
|
* hook re-runs every registered window.Freeday*.initAll() over a subtree after
|
|
3
3
|
* React commits, so [data-fdy-*] markup rendered by React gets enhanced (and
|
|
4
4
|
* re-enhanced after updates). Enhancers are idempotent, so the repeated calls
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* useFreeday(root); // scopes init to this component's subtree
|
|
10
10
|
* // <div ref={root}> ...[data-fdy-*] markup... </div>
|
|
11
11
|
*
|
|
12
|
-
* The ref may also sit ON the widget itself
|
|
12
|
+
* The ref may also sit ON the widget itself, <div ref={root} data-fdy-menu>, which is what you
|
|
13
13
|
* write when the component's root element IS the widget. initAll() matches its own root as
|
|
14
14
|
* well as descendants, so both shapes work. (Before 1.23.0 only the wrapping shape did, and
|
|
15
15
|
* the other failed silently: querySelectorAll never matches its root.)
|
|
16
16
|
*
|
|
17
|
-
* Events are native bubbling CustomEvents
|
|
17
|
+
* Events are native bubbling CustomEvents, add listeners on the root (they
|
|
18
18
|
* bubble) and read event.detail. See index.d.ts for the detail types.
|
|
19
19
|
*/
|
|
20
20
|
import { useEffect, useCallback } from 'react';
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch, type ComputedRef, type Ref } from 'vue';
|
|
3
|
+
import {
|
|
4
|
+
NAV_QUERY,
|
|
5
|
+
applyShellState,
|
|
6
|
+
focusPanel,
|
|
7
|
+
restoreFocus,
|
|
8
|
+
trapTab,
|
|
9
|
+
} from '../../core/app-shell.js';
|
|
10
|
+
|
|
11
|
+
// A Vue wrapper over freeday's `.fdy-app` shell (src/components/app-shell.css) that ships the
|
|
12
|
+
// behaviour the CSS cannot: Escape, backdrop, focus into the panel and back, `inert`, and a Tab
|
|
13
|
+
// trap. The DOM work lives in adapters/core/app-shell.js so this component, its React twin and the
|
|
14
|
+
// Blazor bridge cannot drift into three different focus traps.
|
|
15
|
+
//
|
|
16
|
+
// ONE model: `navOpen` means "the nav is visible to the reader". The two state classes are the
|
|
17
|
+
// kit's business, above the nav breakpoint a hidden nav is `--nav-collapsed`, below it a visible
|
|
18
|
+
// nav is `--nav-open`, so an app never reasons about the viewport to answer a question about its
|
|
19
|
+
// own UI.
|
|
20
|
+
//
|
|
21
|
+
// Leave `navOpen` unbound and the component keeps its own: visible on a wide viewport, hidden on a
|
|
22
|
+
// narrow one, which is the right default and one an app cannot express in a single initial value.
|
|
23
|
+
// Bind it (`v-model:navOpen`) when the app wants to drive it, a menu item, a persisted preference.
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<{
|
|
26
|
+
navOpen?: boolean;
|
|
27
|
+
title?: string;
|
|
28
|
+
toggleLabel?: string;
|
|
29
|
+
}>(), { navOpen: undefined, title: '', toggleLabel: 'Toggle navigation' });
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{
|
|
32
|
+
'update:navOpen': [boolean];
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
const root: Ref<HTMLElement | null> = ref(null);
|
|
36
|
+
const overlay: Ref<boolean> = ref(false);
|
|
37
|
+
const uncontrolled: Ref<boolean> = ref(true);
|
|
38
|
+
const restoreTo: Ref<Element | null> = ref(null);
|
|
39
|
+
let media: MediaQueryList | null = null;
|
|
40
|
+
/* Set while a viewport change is driving the state, so the watcher below reconciles `inert` and the
|
|
41
|
+
classes but leaves FOCUS alone: a resize is not a reader asking to go somewhere. */
|
|
42
|
+
let fromResize: boolean = false;
|
|
43
|
+
|
|
44
|
+
const navVisible: ComputedRef<boolean> = computed((): boolean =>
|
|
45
|
+
props.navOpen === undefined ? uncontrolled.value : props.navOpen,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const shellClass: ComputedRef<string> = computed((): string => {
|
|
49
|
+
if (overlay.value) return navVisible.value ? 'fdy-app fdy-app--nav-open' : 'fdy-app';
|
|
50
|
+
return navVisible.value ? 'fdy-app' : 'fdy-app fdy-app--nav-collapsed';
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function setVisible(next: boolean): void {
|
|
54
|
+
uncontrolled.value = next;
|
|
55
|
+
emit('update:navOpen', next);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Focus is moved AFTER the class change has been rendered, or the panel is still off-canvas and
|
|
59
|
+
the browser refuses to focus what it cannot lay out. */
|
|
60
|
+
watch([navVisible, overlay], ([visible, isOverlay]: [boolean, boolean]): void => {
|
|
61
|
+
const el: HTMLElement | null = root.value;
|
|
62
|
+
if (el === null) return;
|
|
63
|
+
applyShellState(el, { navVisible: visible, overlay: isOverlay });
|
|
64
|
+
if (fromResize) {
|
|
65
|
+
fromResize = false;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!isOverlay) return;
|
|
69
|
+
if (visible) restoreTo.value = focusPanel(el);
|
|
70
|
+
else restoreFocus(el, restoreTo.value);
|
|
71
|
+
}, { flush: 'post' });
|
|
72
|
+
|
|
73
|
+
function onToggle(): void {
|
|
74
|
+
setVisible(!navVisible.value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onBackdrop(): void {
|
|
78
|
+
if (overlay.value && navVisible.value) setVisible(false);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Following a link in an overlay nav means "take me there", the panel must not stay over the page
|
|
82
|
+
it was just asked for. On a wide viewport the nav is a column and clicking it changes nothing. */
|
|
83
|
+
function onSidebarClick(e: MouseEvent): void {
|
|
84
|
+
if (!overlay.value || !navVisible.value) return;
|
|
85
|
+
const target: HTMLElement | null = e.target as HTMLElement | null;
|
|
86
|
+
if (target !== null && target.closest('.fdy-nav__item') !== null) setVisible(false);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function onKeydown(e: KeyboardEvent): void {
|
|
90
|
+
const el: HTMLElement | null = root.value;
|
|
91
|
+
if (el === null || !overlay.value || !navVisible.value) return;
|
|
92
|
+
if (e.key === 'Escape') {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
setVisible(false);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
trapTab(el, e);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Both directions matter, and they are not symmetrical.
|
|
101
|
+
Narrowing with the nav visible would drop an overlay panel over a page nobody asked to leave, so
|
|
102
|
+
the nav is hidden. Widening is harmless, a visible nav simply becomes the column again, and the
|
|
103
|
+
watcher clears the `inert` the overlay had put on the content. */
|
|
104
|
+
function onMediaChange(): void {
|
|
105
|
+
const nowOverlay: boolean = media !== null && !media.matches;
|
|
106
|
+
if (nowOverlay === overlay.value) return;
|
|
107
|
+
fromResize = true;
|
|
108
|
+
overlay.value = nowOverlay;
|
|
109
|
+
if (nowOverlay && navVisible.value) setVisible(false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMounted((): void => {
|
|
113
|
+
media = window.matchMedia(NAV_QUERY);
|
|
114
|
+
overlay.value = !media.matches;
|
|
115
|
+
uncontrolled.value = media.matches;
|
|
116
|
+
media.addEventListener('change', onMediaChange);
|
|
117
|
+
document.addEventListener('keydown', onKeydown);
|
|
118
|
+
const el: HTMLElement | null = root.value;
|
|
119
|
+
if (el !== null) applyShellState(el, { navVisible: navVisible.value, overlay: overlay.value });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
onBeforeUnmount((): void => {
|
|
123
|
+
if (media !== null) media.removeEventListener('change', onMediaChange);
|
|
124
|
+
document.removeEventListener('keydown', onKeydown);
|
|
125
|
+
});
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<template>
|
|
129
|
+
<div ref="root" :class="shellClass">
|
|
130
|
+
<slot name="skip" />
|
|
131
|
+
|
|
132
|
+
<aside class="fdy-app__sidebar" @click="onSidebarClick">
|
|
133
|
+
<slot name="brand" />
|
|
134
|
+
<slot name="nav" />
|
|
135
|
+
</aside>
|
|
136
|
+
|
|
137
|
+
<div class="fdy-app__content">
|
|
138
|
+
<header class="fdy-app__topbar">
|
|
139
|
+
<button
|
|
140
|
+
class="fdy-app__navtoggle"
|
|
141
|
+
type="button"
|
|
142
|
+
:aria-label="toggleLabel"
|
|
143
|
+
@click="onToggle"
|
|
144
|
+
><slot name="toggle-icon">☰</slot></button>
|
|
145
|
+
|
|
146
|
+
<h1 class="fdy-app__title">
|
|
147
|
+
<slot name="title">{{ title }}</slot>
|
|
148
|
+
</h1>
|
|
149
|
+
|
|
150
|
+
<slot name="topbar" />
|
|
151
|
+
</header>
|
|
152
|
+
|
|
153
|
+
<main class="fdy-app__main">
|
|
154
|
+
<slot />
|
|
155
|
+
</main>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="fdy-app__backdrop" @click="onBackdrop" />
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
@@ -3,7 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch, type
|
|
|
3
3
|
import { usePopover } from '../usePopover';
|
|
4
4
|
|
|
5
5
|
// A controlled Vue port of freeday's autocomplete (src/freeday-autocomplete.js +
|
|
6
|
-
// autocomplete.css): a WAI-ARIA APG *editable* combobox
|
|
6
|
+
// autocomplete.css): a WAI-ARIA APG *editable* combobox, a text input filters a listbox of
|
|
7
7
|
// options as you type; picking one fills the input. A real `v-model` in place of the enhancer's
|
|
8
8
|
// DOM mutation, so Vue owns the input. Filtering mirrors the enhancer exactly (case-insensitive
|
|
9
9
|
// substring on the trimmed query); pass server-filtered `options` and it stays a no-op re-filter.
|
|
@@ -25,7 +25,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch, type
|
|
|
25
25
|
import { usePopover } from '../usePopover';
|
|
26
26
|
|
|
27
27
|
// A controlled Vue port of freeday's cascade select (src/freeday-cascade.js + cascade.css):
|
|
28
|
-
// a hierarchical drill-down picker showing one level at a time
|
|
28
|
+
// a hierarchical drill-down picker showing one level at a time, branches drill in, a back
|
|
29
29
|
// control ascends, a leaf selects and the value is the leaf's value (the display is the full
|
|
30
30
|
// path). The enhancer's data model is a hidden nested <ul>; here it is a typed tree, which is
|
|
31
31
|
// what a framework app actually has. A real `v-model` in place of the DOM mutation.
|
|
@@ -9,7 +9,7 @@ import { computed, nextTick, onBeforeUnmount, ref, useId, type ComputedRef, type
|
|
|
9
9
|
// that: a real `v-model:Row|null` over a native <dialog> whose rows come from an async
|
|
10
10
|
// `fetchPage(query, page)`. It re-implements the enhancer's open/close + search + dense
|
|
11
11
|
// sticky-header results + single-commit-on-click + keyboard, and adds the pieces a server
|
|
12
|
-
// picker needs
|
|
12
|
+
// picker needs, loading/empty/error states, retry, pagination, an out-of-order guard, and
|
|
13
13
|
// an optional in-memory cache. The enhancer is NOT mounted here (it would fight Vue's DOM).
|
|
14
14
|
|
|
15
15
|
interface CflColumn {
|
|
@@ -23,7 +23,7 @@ interface CflPage {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const props = defineProps<{
|
|
26
|
-
/** Single: `Row | null`. With `multiple`, an array
|
|
26
|
+
/** Single: `Row | null`. With `multiple`, an array, `Row[] | null`, where null and [] both mean
|
|
27
27
|
* nothing picked. The enhancer has had `data-fdy-cfl-multiple` all along; this is the typed
|
|
28
28
|
* wrappers catching up (#019). */
|
|
29
29
|
modelValue: Row | Row[] | null;
|
|
@@ -31,7 +31,7 @@ const props = defineProps<{
|
|
|
31
31
|
columns: ReadonlyArray<CflColumn>;
|
|
32
32
|
display: (row: Row) => string;
|
|
33
33
|
rowKey: (row: Row) => string;
|
|
34
|
-
/** Advisory only
|
|
34
|
+
/** Advisory only, the caller's `fetchPage` owns paging; kept for API documentation. */
|
|
35
35
|
pageSize?: number;
|
|
36
36
|
/** The dialog's heading. Default 'Choose data', matching the Blazor adapter's `Title`. */
|
|
37
37
|
title?: string;
|
|
@@ -108,7 +108,7 @@ const showClear: ComputedRef<boolean> = computed(
|
|
|
108
108
|
);
|
|
109
109
|
const clearLabelText: ComputedRef<string> = computed((): string => props.clearLabel ?? 'Clear selection');
|
|
110
110
|
const isInvalid: ComputedRef<boolean> = computed((): boolean => props.invalid === true);
|
|
111
|
-
/* `display()` takes one row, so in multi the field states HOW MANY
|
|
111
|
+
/* `display()` takes one row, so in multi the field states HOW MANY, naming one of six would be a
|
|
112
112
|
lie, and naming all six does not fit a control that is 22rem wide. */
|
|
113
113
|
const currentRows: ComputedRef<Row[]> = computed((): Row[] =>
|
|
114
114
|
Array.isArray(props.modelValue) ? props.modelValue : props.modelValue !== null ? [props.modelValue as Row] : [],
|
|
@@ -119,7 +119,7 @@ const displayValue: ComputedRef<string> = computed((): string => {
|
|
|
119
119
|
return props.modelValue !== null ? props.display(props.modelValue as Row) : '';
|
|
120
120
|
});
|
|
121
121
|
// The results <table> (owner of `resultsId`) only renders in the rows branch, so gate the
|
|
122
|
-
// search input's aria refs on rows existing
|
|
122
|
+
// search input's aria refs on rows existing, otherwise they'd dangle during loading/empty/error.
|
|
123
123
|
const hasRows: ComputedRef<boolean> = computed((): boolean => rows.value.length > 0);
|
|
124
124
|
const controlsId: ComputedRef<string | undefined> = computed((): string | undefined =>
|
|
125
125
|
hasRows.value ? resultsId : undefined,
|
|
@@ -156,7 +156,7 @@ async function loadPage(targetPage: number, append: boolean): Promise<void> {
|
|
|
156
156
|
try {
|
|
157
157
|
const cached: CflPage | undefined = cache.get(key);
|
|
158
158
|
const res: CflPage = cached ?? (await props.fetchPage(q, targetPage));
|
|
159
|
-
if (token !== reqToken) return; // stale
|
|
159
|
+
if (token !== reqToken) return; // stale, a newer request has started
|
|
160
160
|
if (cached === undefined) cache.set(key, res);
|
|
161
161
|
const copy: Row[] = res.rows.slice(); // never mutate the caller's array
|
|
162
162
|
rows.value = append ? rows.value.concat(copy) : copy;
|
|
@@ -220,7 +220,7 @@ function togglePick(row: Row): void {
|
|
|
220
220
|
else picked.value = picked.value.filter((_: Row, i: number): boolean => i !== at);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/* A click means "tick this" in multi and "this is my answer" in single
|
|
223
|
+
/* A click means "tick this" in multi and "this is my answer" in single, the whole difference. */
|
|
224
224
|
function onRowClick(row: Row): void {
|
|
225
225
|
if (props.multiple === true) togglePick(row);
|
|
226
226
|
else commit(row);
|
|
@@ -238,7 +238,7 @@ function commit(row: Row | null): void {
|
|
|
238
238
|
closeDialog();
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
/* Unsetting is not "picking nothing"
|
|
241
|
+
/* Unsetting is not "picking nothing", it must not open or close the dialog, and it must leave focus
|
|
242
242
|
on a control that still exists, so focus returns to the trigger beside it. */
|
|
243
243
|
function clearValue(): void {
|
|
244
244
|
picked.value = [];
|
|
@@ -4,7 +4,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick, type Comput
|
|
|
4
4
|
// A Vue-native wrapper over freeday's zero-dependency chart renderer (freeday-chart.js).
|
|
5
5
|
// Unlike the input components, charts are not form controls, so re-implementing them in Vue
|
|
6
6
|
// would only duplicate the SVG maths. Instead this thin wrapper binds the renderer's data-*
|
|
7
|
-
// API reactively and re-invokes window.FreedayChart.update() whenever the data changes
|
|
7
|
+
// API reactively and re-invokes window.FreedayChart.update() whenever the data changes, giving
|
|
8
8
|
// reactive, typed charts (a drop-in replacement for hand-rolled Chart.js wrappers) with no
|
|
9
9
|
// external dependency. Because the renderer paints with CSS var()/--chart-* tokens, charts also
|
|
10
10
|
// recolour on data-theme change for free. Requires the freeday enhancers (dist/freeday.js) to be
|
|
@@ -16,7 +16,7 @@ interface FdyChartSeries {
|
|
|
16
16
|
role?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// Note: no `ariaLabel` prop
|
|
19
|
+
// Note: no `ariaLabel` prop, pass `aria-label` (and any other native attribute) directly; it
|
|
20
20
|
// falls through onto the root element. `role="img"` is set below. Give every chart an aria-label.
|
|
21
21
|
const props = defineProps<{
|
|
22
22
|
type: 'line' | 'area' | 'bar' | 'sparkline' | 'donut';
|
|
@@ -220,7 +220,7 @@ onBeforeUnmount((): void => {
|
|
|
220
220
|
<ul :id="listboxId" ref="listboxEl" class="fdy-combo__listbox" role="listbox" popover="manual" :hidden="!open">
|
|
221
221
|
<!-- @mousedown.prevent keeps focus on the button: the option <li> isn't focusable, so a plain
|
|
222
222
|
mousedown moves focus out of the combo, fires @focusout, and closes the list before the click
|
|
223
|
-
lands
|
|
223
|
+
lands, mouse-select would silently do nothing. Same pattern as FdyDatepicker/FdyAutocomplete. -->
|
|
224
224
|
<li
|
|
225
225
|
v-for="(opt, i) in options"
|
|
226
226
|
:id="optionId(i)"
|
|
@@ -13,7 +13,7 @@ import FdyDatepicker from './FdyDatepicker.vue';
|
|
|
13
13
|
// two linked FdyDatepickers where the end can't precede the start (start.max = end, end.min =
|
|
14
14
|
// start). A real `v-model:DateRangeValue` that composes the single-date picker rather than
|
|
15
15
|
// re-implementing the calendar. The vanilla enhancer's cross-calendar `.in-range` day shading is
|
|
16
|
-
// not reproduced (each picker is independent)
|
|
16
|
+
// not reproduced (each picker is independent), the min/max linkage is what keeps the range valid.
|
|
17
17
|
|
|
18
18
|
const props = defineProps<{
|
|
19
19
|
modelValue: DateRangeValue;
|
|
@@ -21,7 +21,7 @@ const props = defineProps<{
|
|
|
21
21
|
max?: string;
|
|
22
22
|
locale?: string;
|
|
23
23
|
disabled?: boolean;
|
|
24
|
-
/** Locked/view mode for both pickers
|
|
24
|
+
/** Locked/view mode for both pickers, focusable, values shown, but can't be opened or changed. */
|
|
25
25
|
readonly?: boolean;
|
|
26
26
|
invalid?: boolean;
|
|
27
27
|
describedby?: string;
|
|
@@ -39,7 +39,7 @@ const props = defineProps<{
|
|
|
39
39
|
ariaLabelledby?: string;
|
|
40
40
|
/** Show a clear (×) button in the trigger when a date is set, so an optional date can be unset. Emits `''` via update:modelValue + change. Off by default. */
|
|
41
41
|
clearable?: boolean;
|
|
42
|
-
/** aria-label for the previous-month nav button. Default 'Previous month'
|
|
42
|
+
/** aria-label for the previous-month nav button. Default 'Previous month', override for non-English UIs (month/weekday names already follow `locale`). */
|
|
43
43
|
prevMonthLabel?: string;
|
|
44
44
|
/** aria-label for the next-month nav button. Default 'Next month'. */
|
|
45
45
|
nextMonthLabel?: string;
|
|
@@ -145,7 +145,7 @@ const effectiveFocusDate: ComputedRef<Date> = computed((): Date => {
|
|
|
145
145
|
return new Date(view.value.getFullYear(), view.value.getMonth(), Math.min(f.getDate(), daysInMonth));
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
/* 'days' | 'months'
|
|
148
|
+
/* 'days' | 'months', the calendar drills one level up instead of growing furniture beside the title.
|
|
149
149
|
Before this the only pointer route to another month was one click per month: August 2026 to March
|
|
150
150
|
2022 is fifty-three of them, and the Shift+PageUp jump had no affordance at all. */
|
|
151
151
|
const mode: Ref<'days' | 'months' | 'years'> = ref('days');
|
|
@@ -212,7 +212,7 @@ interface YearCell {
|
|
|
212
212
|
focusable: boolean;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
/** A year is unreachable only when EVERY month in it falls outside min/max
|
|
215
|
+
/** A year is unreachable only when EVERY month in it falls outside min/max, the same rule
|
|
216
216
|
* isMonthDisabled applies one level down, and for the same reason. */
|
|
217
217
|
function isYearDisabled(year: number): boolean {
|
|
218
218
|
if (minDate.value !== null && startOfDay(new Date(year, 11, 31)).getTime() < startOfDay(minDate.value).getTime()) return true;
|
|
@@ -411,7 +411,7 @@ function pick(d: Date): void {
|
|
|
411
411
|
closePanel(true);
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
-
// Clear (reset to empty). Emits ''
|
|
414
|
+
// Clear (reset to empty). Emits '', parseISO('') is null, so the placeholder shows again.
|
|
415
415
|
function clearValue(): void {
|
|
416
416
|
emit('update:modelValue', '');
|
|
417
417
|
emit('change', '');
|
|
@@ -496,7 +496,7 @@ function onDocPointerDown(e: MouseEvent): void {
|
|
|
496
496
|
}
|
|
497
497
|
|
|
498
498
|
// Close when focus leaves the control entirely (e.g. Shift+Tab off the trigger).
|
|
499
|
-
/* A null relatedTarget means focus fell to <body
|
|
499
|
+
/* A null relatedTarget means focus fell to <body>, which is what happens when the element the
|
|
500
500
|
* user just pressed is REMOVED by the click it triggered (drilling into the month grid replaces
|
|
501
501
|
* the grid, and with it the cell that had focus). That is not focus leaving the control, and
|
|
502
502
|
* closing on it made the panel vanish mid-navigation. A pointer that really lands outside is
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } from 'vue';
|
|
3
3
|
|
|
4
4
|
// A controlled Vue wrapper over freeday's `.fdy-drawer` native <dialog> side panel
|
|
5
|
-
// (src/components/drawer.css). Same controlled contract and glue as FdyModal
|
|
5
|
+
// (src/components/drawer.css). Same controlled contract and glue as FdyModal, showModal()/close()
|
|
6
6
|
// guarded, @cancel.prevent so Esc routes through app state, backdrop-click via `event.target ===
|
|
7
|
-
// dialogEl
|
|
7
|
+
// dialogEl`, applied to a drawer that anchors left (default) or right. Native <dialog> supplies the
|
|
8
8
|
// focus trap, focus restore, top-layer stacking and inert background; `dismissible` (default true)
|
|
9
9
|
// gates Esc + backdrop dismissal.
|
|
10
10
|
|
|
11
11
|
// dismissible MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop
|
|
12
12
|
// `false`, not `undefined`, so a bare `props.dismissible !== false` would make an un-annotated
|
|
13
|
-
// drawer non-dismissible (no Esc, no backdrop, no close button)
|
|
13
|
+
// drawer non-dismissible (no Esc, no backdrop, no close button), the opposite of the default.
|
|
14
14
|
const props = withDefaults(defineProps<{
|
|
15
15
|
open: boolean;
|
|
16
16
|
title: string;
|
|
@@ -8,11 +8,11 @@ import { computed, onMounted, useId, watch, type ComputedRef, type Ref, ref } fr
|
|
|
8
8
|
// (showModal() on an open dialog throws), @cancel.prevent so Esc routes through app state instead of
|
|
9
9
|
// closing the DOM behind its back, and backdrop-click detection via `event.target === dialogEl`.
|
|
10
10
|
// Native <dialog> already provides the focus trap, focus restore, top-layer stacking and inert
|
|
11
|
-
// background
|
|
11
|
+
// background, the wrapper only avoids breaking them. `dismissible` (default true) gates Esc + backdrop.
|
|
12
12
|
|
|
13
13
|
// dismissible MUST go through withDefaults: Vue's boolean-cast gives an omitted Boolean prop
|
|
14
14
|
// `false`, not `undefined`, so a bare `props.dismissible !== false` would make an un-annotated
|
|
15
|
-
// modal non-dismissible (no Esc, no backdrop, no close button)
|
|
15
|
+
// modal non-dismissible (no Esc, no backdrop, no close button), the opposite of the default.
|
|
16
16
|
const props = withDefaults(defineProps<{
|
|
17
17
|
open: boolean;
|
|
18
18
|
title: string;
|
|
@@ -46,7 +46,7 @@ onMounted((): void => sync(props.open));
|
|
|
46
46
|
function onCancel(): void {
|
|
47
47
|
if (dismissible.value) emit('close');
|
|
48
48
|
}
|
|
49
|
-
// The ::backdrop is not a separate element
|
|
49
|
+
// The ::backdrop is not a separate element, a click whose target is the dialog box itself (not its
|
|
50
50
|
// content) is a backdrop click.
|
|
51
51
|
function onClick(e: MouseEvent): void {
|
|
52
52
|
if (dismissible.value && e.target === dialogEl.value) emit('close');
|
|
@@ -45,14 +45,14 @@ const props = withDefaults(defineProps<{
|
|
|
45
45
|
/** Client-side page size when `page` is absent; 0/undefined = render all rows (no pager). */
|
|
46
46
|
pageSize?: number;
|
|
47
47
|
/**
|
|
48
|
-
* Controlled client-side page index (0-based). Provide it
|
|
48
|
+
* Controlled client-side page index (0-based). Provide it, with `pageSize`, without `page`, to
|
|
49
49
|
* own the page while the table keeps doing filter/sort/paginate. This is what lets an EXTERNAL
|
|
50
50
|
* pager drive the table: a responsive screen that hides `.fdy-datatable` below `md` and renders a
|
|
51
51
|
* card list from the `process` event can render one pager for both breakpoints and point it here.
|
|
52
52
|
* Omit for the internal index (unchanged default).
|
|
53
53
|
*/
|
|
54
54
|
/** Withhold the table's own footer (pager + range) so the screen can render one. Server mode had
|
|
55
|
-
* no way to do this: the app owns the page there ANYWAY, and was still handed a second control
|
|
55
|
+
* no way to do this: the app owns the page there ANYWAY, and was still handed a second control,
|
|
56
56
|
* a responsive list that shows a table at one breakpoint and cards at another ended up with the
|
|
57
57
|
* kit's pager stacked under its own. Client mode's counterpart is `pageIndex`. Default true. */
|
|
58
58
|
pager?: boolean;
|
|
@@ -61,7 +61,7 @@ const props = withDefaults(defineProps<{
|
|
|
61
61
|
* (unchanged default). Every back office has one, and a table that renders two thirds of its own
|
|
62
62
|
* footer forces the app to rebuild all three to add the last (#008).
|
|
63
63
|
*
|
|
64
|
-
* Server mode reports the pick through `update:page
|
|
64
|
+
* Server mode reports the pick through `update:page`, same event as a page click, with a new
|
|
65
65
|
* `size`. Client mode keeps it internally and also emits `update:pageSize`, so the control works
|
|
66
66
|
* with nothing wired and can still be persisted.
|
|
67
67
|
*/
|
|
@@ -86,11 +86,11 @@ const emit = defineEmits<{
|
|
|
86
86
|
* a reset to 0 after sort/filter, or a clamp when filtering shrank the set). */
|
|
87
87
|
'update:pageIndex': [index: number];
|
|
88
88
|
/** Client mode with `pageSizes`: the reader picked a new rows-per-page. The table has already
|
|
89
|
-
* applied it
|
|
89
|
+
* applied it, this is for a caller that wants to persist the choice. */
|
|
90
90
|
'update:pageSize': [size: number];
|
|
91
91
|
/** A row was activated (click, or Enter/Space while the row itself is focused). */
|
|
92
92
|
'row-activate': [row: Row];
|
|
93
|
-
/** The processed page of rows (after filter/sort/paginate) plus the total row count
|
|
93
|
+
/** The processed page of rows (after filter/sort/paginate) plus the total row count, fires in
|
|
94
94
|
* BOTH modes whenever they change. Lets a consumer render the SAME processed set elsewhere
|
|
95
95
|
* (a `< md` card list, a "selected" summary, export-to-CSV) without re-deriving the pipeline. */
|
|
96
96
|
'process': [result: { rows: Row[]; total: number }];
|
|
@@ -100,7 +100,7 @@ const internalSort: Ref<FdySortState | null> = ref(null);
|
|
|
100
100
|
const internalFilters: Ref<FdyFilterMap> = ref({});
|
|
101
101
|
const internalPageIndex: Ref<number> = ref(0);
|
|
102
102
|
/* Client-mode rows-per-page. `pageSize` is a plain prop with no event, so a footer control that only
|
|
103
|
-
* emitted would do nothing in the app that wired nothing
|
|
103
|
+
* emitted would do nothing in the app that wired nothing, the table applies the pick itself and
|
|
104
104
|
* reports it. An explicit change to the prop wins back: the parent said something newer. */
|
|
105
105
|
const internalPageSize: Ref<number | null> = ref(null);
|
|
106
106
|
watch((): number | undefined => props.pageSize, (): void => { internalPageSize.value = null; });
|
|
@@ -250,7 +250,7 @@ function rowClasses(row: Row): Array<string | undefined> {
|
|
|
250
250
|
function onRowClick(row: Row): void {
|
|
251
251
|
if (props.rowActivatable === true) emit('row-activate', row);
|
|
252
252
|
}
|
|
253
|
-
// Enter/Space activate only when the row itself is focused
|
|
253
|
+
// Enter/Space activate only when the row itself is focused, a control inside a cell keeps its own
|
|
254
254
|
// event (the `event.target !== event.currentTarget` guard). Click relies on inner controls calling
|
|
255
255
|
// stopPropagation, matching the pattern consumers hand-roll today.
|
|
256
256
|
function onRowKeydown(e: KeyboardEvent, row: Row): void {
|
|
@@ -7,7 +7,7 @@ import type { FdyColumnFilter, FdyColumnFilterType } from '../../core/table-mode
|
|
|
7
7
|
// Internal to FdyTable: one column's header funnel button + its type-aware filter popover
|
|
8
8
|
// (text / enum / number / date) over freeday's `.fdy-table__filterbtn` + `.fdy-filter*` classes.
|
|
9
9
|
// Reuses usePopover so the panel escapes the table's `overflow:hidden` via the top layer. Purely
|
|
10
|
-
// controlled
|
|
10
|
+
// controlled, it renders the current `filter` and emits the next one (or null to clear); the
|
|
11
11
|
// parent owns where that goes (client state or an `update:filters` emit). Not exported.
|
|
12
12
|
|
|
13
13
|
const props = defineProps<{
|