@svgrid/grid 2.2.16 → 2.2.18
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 +1 -1
- package/dist/GridFooter.svelte +12 -11
- package/dist/SvAnchor.svelte +65 -0
- package/dist/SvAnchor.svelte.d.ts +24 -0
- package/dist/SvAspectRatio.svelte +38 -0
- package/dist/SvAspectRatio.svelte.d.ts +16 -0
- package/dist/SvAutoComplete.svelte +52 -7
- package/dist/SvAutoComplete.svelte.d.ts +4 -1
- package/dist/SvBlockquote.svelte +43 -0
- package/dist/SvBlockquote.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +33 -2
- package/dist/SvCalendar.svelte.d.ts +8 -1
- package/dist/SvCheckBox.svelte +6 -2
- package/dist/SvCode.svelte +52 -0
- package/dist/SvCode.svelte.d.ts +19 -0
- package/dist/SvCollapsible.svelte +75 -0
- package/dist/SvCollapsible.svelte.d.ts +27 -0
- package/dist/SvColorInput.svelte +11 -17
- package/dist/SvColorInput.svelte.d.ts +2 -0
- package/dist/SvComboBox.svelte +85 -8
- package/dist/SvComboBox.svelte.d.ts +14 -0
- package/dist/SvCountryInput.svelte +2 -2
- package/dist/SvCountryInput.svelte.d.ts +1 -1
- package/dist/SvDropDownList.svelte +154 -51
- package/dist/SvDropDownList.svelte.d.ts +17 -3
- package/dist/SvDurationInput.svelte +59 -41
- package/dist/SvDurationInput.svelte.d.ts +18 -0
- package/dist/SvField.svelte +203 -12
- package/dist/SvField.svelte.d.ts +45 -8
- package/dist/SvForm.svelte +195 -21
- package/dist/SvForm.svelte.d.ts +23 -0
- package/dist/SvGrid.controller.svelte.d.ts +12 -1
- package/dist/SvGrid.controller.svelte.js +87 -7
- package/dist/SvGrid.svelte +383 -181
- package/dist/SvGrid.types.d.ts +123 -2
- package/dist/SvGridChartView.svelte +70 -0
- package/dist/SvGridChartView.svelte.d.ts +11 -0
- package/dist/SvGridDropdown.svelte +9 -2
- package/dist/SvGridSelect.svelte +5 -2
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvGroup.svelte +41 -0
- package/dist/SvGroup.svelte.d.ts +27 -0
- package/dist/SvHoverCard.svelte +70 -0
- package/dist/SvHoverCard.svelte.d.ts +41 -0
- package/dist/SvKbd.svelte +51 -0
- package/dist/SvKbd.svelte.d.ts +18 -0
- package/dist/SvList.svelte +55 -0
- package/dist/SvList.svelte.d.ts +23 -0
- package/dist/SvListBox.svelte +166 -77
- package/dist/SvListBox.svelte.d.ts +17 -4
- package/dist/SvLoadingOverlay.svelte +53 -0
- package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
- package/dist/SvMark.svelte +36 -0
- package/dist/SvMark.svelte.d.ts +14 -0
- package/dist/SvMaskedInput.svelte +50 -35
- package/dist/SvMaskedInput.svelte.d.ts +17 -2
- package/dist/SvMenuList.svelte +32 -3
- package/dist/SvMenubar.svelte +200 -0
- package/dist/SvMenubar.svelte.d.ts +18 -0
- package/dist/SvMultiSelect.svelte +7 -4
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +79 -42
- package/dist/SvNumberInput.svelte.d.ts +25 -0
- package/dist/SvOtpInput.svelte +2 -2
- package/dist/SvPasswordInput.svelte +51 -31
- package/dist/SvPasswordInput.svelte.d.ts +17 -0
- package/dist/SvPhoneInput.svelte +39 -28
- package/dist/SvPhoneInput.svelte.d.ts +4 -0
- package/dist/SvPopconfirm.svelte +79 -0
- package/dist/SvPopconfirm.svelte.d.ts +42 -0
- package/dist/SvPopover.svelte +87 -29
- package/dist/SvPopover.svelte.d.ts +18 -4
- package/dist/SvRadioGroup.svelte +5 -1
- package/dist/SvResult.svelte +73 -0
- package/dist/SvResult.svelte.d.ts +28 -0
- package/dist/SvSegmented.svelte +114 -0
- package/dist/SvSegmented.svelte.d.ts +29 -0
- package/dist/SvSimpleGrid.svelte +41 -0
- package/dist/SvSimpleGrid.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +23 -10
- package/dist/SvSpinner.svelte +43 -0
- package/dist/SvSpinner.svelte.d.ts +20 -0
- package/dist/SvStack.svelte +37 -0
- package/dist/SvStack.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +6 -1
- package/dist/SvTagsInput.svelte +9 -5
- package/dist/SvText.svelte +76 -0
- package/dist/SvText.svelte.d.ts +31 -0
- package/dist/SvTextInput.svelte +75 -43
- package/dist/SvTextInput.svelte.d.ts +28 -1
- package/dist/SvTimePicker.svelte +32 -2
- package/dist/SvTimePicker.svelte.d.ts +8 -9
- package/dist/SvTitle.svelte +57 -0
- package/dist/SvTitle.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +41 -6
- package/dist/SvToggleButton.svelte +4 -1
- package/dist/SvTooltip.svelte +68 -34
- package/dist/SvTooltip.svelte.d.ts +14 -2
- package/dist/SvTree.svelte +81 -21
- package/dist/SvTree.svelte.d.ts +3 -2
- package/dist/SvTreeSelect.svelte +5 -2
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/SvVisuallyHidden.svelte +49 -0
- package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
- package/dist/ai.d.ts +289 -0
- package/dist/ai.js +1013 -0
- package/dist/board-view.svelte.d.ts +22 -0
- package/dist/board-view.svelte.js +13 -0
- package/dist/build-api.js +36 -6
- package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
- package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
- package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
- package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
- package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
- package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
- package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
- package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
- package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
- package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
- package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
- package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
- package/dist/cdn/chart-T1usDN0o.js +1212 -0
- package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
- package/dist/cdn/export-format-JT5pWVT8.js +561 -0
- package/dist/cdn/popover-P-9P2YYn.js +69 -0
- package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/cdn/src-BLhdLmoN.js +25707 -0
- package/dist/cdn/src-BwnUVj3T.js +25623 -0
- package/dist/cdn/svgrid.js +9 -32897
- package/dist/cdn/svgrid.svelte-external.js +9 -30052
- package/dist/cell-formatting.d.ts +2 -2
- package/dist/cell-render.js +4 -4
- package/dist/chart-view.svelte.d.ts +25 -0
- package/dist/chart-view.svelte.js +14 -0
- package/dist/createCombobox.svelte.d.ts +5 -0
- package/dist/createCombobox.svelte.js +21 -5
- package/dist/createDropdownList.svelte.d.ts +3 -0
- package/dist/createDropdownList.svelte.js +8 -2
- package/dist/createForm.svelte.d.ts +18 -0
- package/dist/createForm.svelte.js +51 -6
- package/dist/createMenu.svelte.js +27 -0
- package/dist/createPopoverSelect.svelte.d.ts +3 -0
- package/dist/createPopoverSelect.svelte.js +9 -1
- package/dist/createSlider.svelte.d.ts +8 -21
- package/dist/createSlider.svelte.js +34 -0
- package/dist/createTooltip.svelte.d.ts +12 -0
- package/dist/createTooltip.svelte.js +72 -9
- package/dist/editor-contract.d.ts +15 -0
- package/dist/export-provider.d.ts +27 -0
- package/dist/export-provider.js +9 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +33 -0
- package/dist/form-field.d.ts +45 -2
- package/dist/grid-messages.d.ts +61 -0
- package/dist/grid-messages.js +72 -0
- package/dist/group-display.d.ts +62 -0
- package/dist/group-display.js +78 -0
- package/dist/index.d.ts +36 -4
- package/dist/index.js +43 -2
- package/dist/js-scroller.svelte.d.ts +35 -0
- package/dist/js-scroller.svelte.js +154 -0
- package/dist/list-option.d.ts +46 -0
- package/dist/list-option.js +53 -0
- package/dist/menus.js +25 -11
- package/dist/pivot-view.svelte.d.ts +72 -0
- package/dist/pivot-view.svelte.js +13 -0
- package/dist/positioning.d.ts +111 -0
- package/dist/positioning.js +191 -0
- package/dist/scheduler-model.d.ts +3 -0
- package/dist/scheduler-model.js +22 -5
- package/dist/svgrid-wrapper.types.d.ts +18 -0
- package/dist/toast-store.svelte.d.ts +54 -0
- package/dist/toast-store.svelte.js +65 -15
- package/package.json +9 -1
- package/src/GridFooter.svelte +12 -11
- package/src/SvAnchor.svelte +65 -0
- package/src/SvAspectRatio.svelte +38 -0
- package/src/SvAutoComplete.svelte +52 -7
- package/src/SvBlockquote.svelte +43 -0
- package/src/SvCalendar.svelte +33 -2
- package/src/SvCalendar.test.ts +15 -0
- package/src/SvCheckBox.svelte +6 -2
- package/src/SvCode.svelte +52 -0
- package/src/SvCollapsible.svelte +75 -0
- package/src/SvColorInput.svelte +11 -17
- package/src/SvComboBox.svelte +85 -8
- package/src/SvCountryInput.svelte +2 -2
- package/src/SvDropDownList.svelte +154 -51
- package/src/SvDurationInput.svelte +59 -41
- package/src/SvField.svelte +203 -12
- package/src/SvForm.svelte +195 -21
- package/src/SvForm.test.ts +112 -0
- package/src/SvGrid.controller.svelte.ts +88 -7
- package/src/SvGrid.svelte +383 -181
- package/src/SvGrid.types.ts +125 -1
- package/src/SvGridChartView.svelte +70 -0
- package/src/SvGridDropdown.svelte +9 -2
- package/src/SvGridSelect.svelte +5 -2
- package/src/SvGroup.svelte +41 -0
- package/src/SvHoverCard.svelte +70 -0
- package/src/SvKbd.svelte +51 -0
- package/src/SvList.svelte +55 -0
- package/src/SvListBox.svelte +166 -77
- package/src/SvLoadingOverlay.svelte +53 -0
- package/src/SvMark.svelte +36 -0
- package/src/SvMaskedInput.svelte +50 -35
- package/src/SvMenuList.svelte +32 -3
- package/src/SvMenubar.svelte +200 -0
- package/src/SvMultiSelect.svelte +7 -4
- package/src/SvNumberInput.svelte +79 -42
- package/src/SvOtpInput.svelte +2 -2
- package/src/SvPasswordInput.svelte +51 -31
- package/src/SvPhoneInput.svelte +39 -28
- package/src/SvPopconfirm.svelte +79 -0
- package/src/SvPopover.svelte +87 -29
- package/src/SvRadioGroup.svelte +5 -1
- package/src/SvResult.svelte +73 -0
- package/src/SvSegmented.svelte +114 -0
- package/src/SvSimpleGrid.svelte +41 -0
- package/src/SvSlider.svelte +23 -10
- package/src/SvSpinner.svelte +43 -0
- package/src/SvStack.svelte +37 -0
- package/src/SvSwitchButton.svelte +6 -1
- package/src/SvTagsInput.svelte +9 -5
- package/src/SvText.svelte +76 -0
- package/src/SvTextInput.svelte +75 -43
- package/src/SvTimePicker.svelte +32 -2
- package/src/SvTitle.svelte +57 -0
- package/src/SvToaster.svelte +41 -6
- package/src/SvToaster.test.ts +32 -0
- package/src/SvToggleButton.svelte +4 -1
- package/src/SvTooltip.svelte +68 -34
- package/src/SvTree.svelte +81 -21
- package/src/SvTreeSelect.svelte +5 -2
- package/src/SvVisuallyHidden.svelte +49 -0
- package/src/ai.test.ts +502 -0
- package/src/ai.ts +1391 -0
- package/src/board-view.svelte.ts +33 -0
- package/src/build-api.coverage.test.ts +101 -0
- package/src/build-api.ts +43 -18
- package/src/builtin-editors.grid.test.ts +1 -1
- package/src/cell-formatting.ts +5 -5
- package/src/cell-render.ts +4 -4
- package/src/chart-view.svelte.ts +36 -0
- package/src/createCombobox.svelte.ts +17 -5
- package/src/createDropdownList.svelte.ts +8 -2
- package/src/createForm.svelte.ts +59 -6
- package/src/createMenu.svelte.ts +23 -0
- package/src/createPopoverSelect.svelte.ts +8 -1
- package/src/createSlider.svelte.ts +20 -0
- package/src/createTooltip.svelte.ts +67 -9
- package/src/editor-contract.ts +16 -0
- package/src/export-provider.ts +40 -0
- package/src/filter-operators.ts +37 -0
- package/src/form-field.ts +42 -2
- package/src/grid-messages.test.ts +24 -0
- package/src/grid-messages.ts +137 -0
- package/src/group-display.test.ts +77 -0
- package/src/group-display.ts +117 -0
- package/src/i18n.grid.test.ts +73 -0
- package/src/index.ts +91 -2
- package/src/js-scroller.svelte.ts +173 -0
- package/src/list-option.test.ts +56 -0
- package/src/list-option.ts +81 -0
- package/src/menus.test.ts +32 -0
- package/src/menus.ts +25 -12
- package/src/pivot-view.svelte.ts +90 -0
- package/src/pivot-view.test.ts +38 -0
- package/src/pivot.grid.test.ts +102 -0
- package/src/positioning.test.ts +141 -0
- package/src/positioning.ts +266 -0
- package/src/scheduler-model.test.ts +16 -0
- package/src/scheduler-model.ts +24 -5
- package/src/svgrid-wrapper.types.ts +23 -0
- package/src/svgrid.charting.test.ts +8 -1
- package/src/svgrid.comments-autocomplete.test.ts +7 -2
- package/src/svgrid.context-menu.test.ts +8 -3
- package/src/toast-store.svelte.ts +98 -0
- package/src/toast-store.test.ts +62 -1
- package/src/ui-breadth.test.ts +138 -0
- package/src/ui-buttons.test.ts +39 -0
- package/src/ui-inputs.test.ts +2 -2
- package/src/ui-range.test.ts +9 -0
- package/src/ui-selection.test.ts +36 -0
- package/src/ui-tier1.test.ts +1 -1
- package/src/virtual.test.ts +20 -6
- package/dist/SvGridBoard.svelte +0 -2339
- package/dist/SvGridBoard.svelte.d.ts +0 -31
- package/src/SvGridBoard.svelte +0 -2339
- package/src/board.test.ts +0 -927
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
export declare function resolveDatePattern(pattern: string | undefined, kind: 'date' | 'datetime'): Intl.DateTimeFormatOptions | undefined;
|
|
6
6
|
type NumericFormatInput = {
|
|
7
7
|
type: 'number' | 'currency' | 'percent';
|
|
8
|
-
locales?: string | string[];
|
|
8
|
+
locales?: string | readonly string[];
|
|
9
9
|
currency?: string;
|
|
10
10
|
/** When type is percent: treat value as 0–100 instead of Intl’s 0–1 fraction */
|
|
11
11
|
valueIsPercentPoints?: boolean;
|
|
12
12
|
options?: Intl.NumberFormatOptions;
|
|
13
13
|
};
|
|
14
|
-
export declare function getDateFormatter(locales: string | string[] | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
|
|
14
|
+
export declare function getDateFormatter(locales: string | readonly string[] | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
|
|
15
15
|
/** Format a numeric value for number / currency / percent column formats. */
|
|
16
16
|
export declare function formatNumericWithConfig(value: unknown, config: NumericFormatInput): string;
|
|
17
17
|
export {};
|
package/dist/cell-render.js
CHANGED
|
@@ -172,7 +172,7 @@ export function createCellRender(ctx) {
|
|
|
172
172
|
formatConfig.type === "percent") {
|
|
173
173
|
return formatNumericWithConfig(value, {
|
|
174
174
|
type: formatConfig.type,
|
|
175
|
-
locales: formatConfig.locales,
|
|
175
|
+
locales: (formatConfig.locales ?? ctx.props.localization?.locale),
|
|
176
176
|
currency: formatConfig.type === "currency"
|
|
177
177
|
? (formatConfig.currency ?? "USD")
|
|
178
178
|
: undefined,
|
|
@@ -197,7 +197,7 @@ export function createCellRender(ctx) {
|
|
|
197
197
|
hour: "2-digit",
|
|
198
198
|
minute: "2-digit",
|
|
199
199
|
};
|
|
200
|
-
return getDateFormatter(formatConfig.locales, merged).format(parsedDate);
|
|
200
|
+
return getDateFormatter((formatConfig.locales ?? ctx.props.localization?.locale), merged).format(parsedDate);
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
return String(value ?? "");
|
|
@@ -226,7 +226,7 @@ export function createCellRender(ctx) {
|
|
|
226
226
|
formatConfig.type === "percent") {
|
|
227
227
|
return formatNumericWithConfig(value, {
|
|
228
228
|
type: formatConfig.type,
|
|
229
|
-
locales: formatConfig.locales,
|
|
229
|
+
locales: (formatConfig.locales ?? ctx.props.localization?.locale),
|
|
230
230
|
currency: formatConfig.type === "currency"
|
|
231
231
|
? (formatConfig.currency ?? "USD")
|
|
232
232
|
: undefined,
|
|
@@ -245,7 +245,7 @@ export function createCellRender(ctx) {
|
|
|
245
245
|
: formatConfig.type === "date"
|
|
246
246
|
? { year: "numeric", month: "2-digit", day: "2-digit" }
|
|
247
247
|
: { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" };
|
|
248
|
-
return getDateFormatter(formatConfig.locales, merged).format(parsedDate);
|
|
248
|
+
return getDateFormatter((formatConfig.locales ?? ctx.props.localization?.locale), merged).format(parsedDate);
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
return String(value ?? "");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chart-view registry - the injection seam for the *chart view of the grid*.
|
|
3
|
+
* Setting the `chart` prop on `<SvGrid>` swaps the table for a chart driven by
|
|
4
|
+
* the grid's filtered + sorted rows. Mirrors the `board-view` / `scheduler-view`
|
|
5
|
+
* seam, with one difference: the chart renderer is NOT a paid feature. The chart
|
|
6
|
+
* primitive (`SvChart` / `SvGridChart`) already ships in the free `@svgrid/grid`,
|
|
7
|
+
* so the grid lazy-loads a built-in default renderer (`SvGridChartView`) out of
|
|
8
|
+
* the box. This seam exists only so a host (or enterprise) can *override* that
|
|
9
|
+
* default with a richer renderer - it is not required for the view to work.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // optional: replace the built-in chart view with your own renderer
|
|
13
|
+
* import { registerChartView } from '@svgrid/grid'
|
|
14
|
+
* import MyChartView from './MyChartView.svelte'
|
|
15
|
+
* registerChartView(MyChartView)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Component } from 'svelte';
|
|
19
|
+
/** Override the built-in chart-view renderer. Optional - the grid ships a
|
|
20
|
+
* free default, so this is only for hosts that want a custom renderer. */
|
|
21
|
+
export declare function registerChartView(component: Component<any>): void;
|
|
22
|
+
/** The registered chart-view override, or null to use the built-in default. */
|
|
23
|
+
export declare function getChartView(): Component<any> | null;
|
|
24
|
+
/** Whether a chart-view override has been registered. */
|
|
25
|
+
export declare function hasChartView(): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
let renderer = $state(null);
|
|
2
|
+
/** Override the built-in chart-view renderer. Optional - the grid ships a
|
|
3
|
+
* free default, so this is only for hosts that want a custom renderer. */
|
|
4
|
+
export function registerChartView(component) {
|
|
5
|
+
renderer = component;
|
|
6
|
+
}
|
|
7
|
+
/** The registered chart-view override, or null to use the built-in default. */
|
|
8
|
+
export function getChartView() {
|
|
9
|
+
return renderer;
|
|
10
|
+
}
|
|
11
|
+
/** Whether a chart-view override has been registered. */
|
|
12
|
+
export function hasChartView() {
|
|
13
|
+
return renderer != null;
|
|
14
|
+
}
|
|
@@ -27,6 +27,8 @@ export type ComboboxConfig = {
|
|
|
27
27
|
value: () => ComboboxValue;
|
|
28
28
|
onChange?: (value: ComboboxValue) => void;
|
|
29
29
|
disabled?: () => boolean;
|
|
30
|
+
/** Read-only: value shown, input focusable, but not editable and the panel will not open. */
|
|
31
|
+
readonly?: () => boolean;
|
|
30
32
|
ariaLabel?: () => string | undefined;
|
|
31
33
|
/** Filter the options locally by the query. Set false for server-side search. */
|
|
32
34
|
localFilter?: () => boolean;
|
|
@@ -65,13 +67,16 @@ export declare function createCombobox(config: ComboboxConfig): {
|
|
|
65
67
|
close: (revert?: boolean) => void;
|
|
66
68
|
toggle: () => void;
|
|
67
69
|
pick: (o: ListOption | undefined) => void;
|
|
70
|
+
clear: () => void;
|
|
68
71
|
onInput: (e: Event) => void;
|
|
69
72
|
onFocus: () => void;
|
|
70
73
|
onKeydown: (e: KeyboardEvent) => void;
|
|
71
74
|
/** Spread onto the editable <input>. */
|
|
72
75
|
inputProps: () => {
|
|
76
|
+
'aria-readonly': true | undefined;
|
|
73
77
|
value: string;
|
|
74
78
|
disabled: boolean;
|
|
79
|
+
readonly: true | undefined;
|
|
75
80
|
oninput: (e: Event) => void;
|
|
76
81
|
onfocus: () => void;
|
|
77
82
|
onkeydown: (e: KeyboardEvent) => void;
|
|
@@ -27,6 +27,7 @@ export function createCombobox(config) {
|
|
|
27
27
|
const listId = `${id}-list`;
|
|
28
28
|
const opts = () => config.options();
|
|
29
29
|
const disabled = () => config.disabled?.() ?? false;
|
|
30
|
+
const readonly = () => config.readonly?.() ?? false;
|
|
30
31
|
let open = $state(false);
|
|
31
32
|
let query = $state('');
|
|
32
33
|
let active = $state(0);
|
|
@@ -47,7 +48,7 @@ export function createCombobox(config) {
|
|
|
47
48
|
hint: config.hint?.(),
|
|
48
49
|
ariaLabel: config.ariaLabel?.(),
|
|
49
50
|
});
|
|
50
|
-
function openPanel() { if (disabled() || open)
|
|
51
|
+
function openPanel() { if (disabled() || readonly() || open)
|
|
51
52
|
return; open = true; active = 0; }
|
|
52
53
|
function close(revert = true) {
|
|
53
54
|
open = false;
|
|
@@ -63,7 +64,7 @@ export function createCombobox(config) {
|
|
|
63
64
|
openPanel();
|
|
64
65
|
} }
|
|
65
66
|
function pick(o) {
|
|
66
|
-
if (!o || o.disabled)
|
|
67
|
+
if (readonly() || !o || o.disabled)
|
|
67
68
|
return;
|
|
68
69
|
config.onChange?.(o.value);
|
|
69
70
|
query = o.label;
|
|
@@ -71,18 +72,30 @@ export function createCombobox(config) {
|
|
|
71
72
|
open = false;
|
|
72
73
|
config.blurInput?.();
|
|
73
74
|
}
|
|
75
|
+
/** Clear the selection (value -> null) and reset the shown text. */
|
|
76
|
+
function clear() {
|
|
77
|
+
if (readonly() || disabled())
|
|
78
|
+
return;
|
|
79
|
+
config.onChange?.(null);
|
|
80
|
+
query = '';
|
|
81
|
+
editing = false;
|
|
82
|
+
open = false;
|
|
83
|
+
}
|
|
74
84
|
function setActive(i) { const o = filtered[i]; if (o && !o.disabled)
|
|
75
85
|
active = i; }
|
|
76
86
|
function onInput(e) {
|
|
87
|
+
if (readonly())
|
|
88
|
+
return;
|
|
77
89
|
query = e.currentTarget.value;
|
|
78
90
|
editing = true;
|
|
79
91
|
active = 0;
|
|
80
92
|
if (!open)
|
|
81
93
|
openPanel();
|
|
82
94
|
}
|
|
83
|
-
function onFocus() {
|
|
95
|
+
function onFocus() { if (readonly())
|
|
96
|
+
return; editing = true; query = selected?.label ?? ''; openPanel(); }
|
|
84
97
|
function onKeydown(e) {
|
|
85
|
-
if (disabled())
|
|
98
|
+
if (disabled() || readonly())
|
|
86
99
|
return;
|
|
87
100
|
if (!open && e.key === 'ArrowDown') {
|
|
88
101
|
e.preventDefault();
|
|
@@ -133,6 +146,7 @@ export function createCombobox(config) {
|
|
|
133
146
|
close,
|
|
134
147
|
toggle,
|
|
135
148
|
pick,
|
|
149
|
+
clear,
|
|
136
150
|
onInput,
|
|
137
151
|
onFocus,
|
|
138
152
|
onKeydown,
|
|
@@ -145,8 +159,10 @@ export function createCombobox(config) {
|
|
|
145
159
|
'aria-autocomplete': 'list',
|
|
146
160
|
'aria-activedescendant': open && filtered[active] ? optionId(active) : undefined,
|
|
147
161
|
...editorAria(ariaState()),
|
|
162
|
+
'aria-readonly': readonly() || undefined,
|
|
148
163
|
value: shownText,
|
|
149
164
|
disabled: disabled(),
|
|
165
|
+
readonly: readonly() || undefined,
|
|
150
166
|
oninput: onInput,
|
|
151
167
|
onfocus: onFocus,
|
|
152
168
|
onkeydown: onKeydown,
|
|
@@ -156,7 +172,7 @@ export function createCombobox(config) {
|
|
|
156
172
|
type: 'button',
|
|
157
173
|
tabindex: -1,
|
|
158
174
|
'aria-label': 'Toggle',
|
|
159
|
-
disabled: disabled(),
|
|
175
|
+
disabled: disabled() || readonly(),
|
|
160
176
|
onclick: toggle,
|
|
161
177
|
}),
|
|
162
178
|
/** Spread onto the listbox container. */
|
|
@@ -27,6 +27,8 @@ export type DropdownListConfig = {
|
|
|
27
27
|
value: () => DropdownValue;
|
|
28
28
|
onChange?: (value: string | number) => void;
|
|
29
29
|
disabled?: () => boolean;
|
|
30
|
+
/** Read-only: value shown, trigger focusable, but the panel will not open. */
|
|
31
|
+
readonly?: () => boolean;
|
|
30
32
|
ariaLabel?: () => string | undefined;
|
|
31
33
|
/** DOM focus hook provided by the renderer; the core never touches the DOM. */
|
|
32
34
|
focusTrigger?: () => void;
|
|
@@ -56,6 +58,7 @@ export declare function createDropdownList(config: DropdownListConfig): {
|
|
|
56
58
|
onKeydown: (e: KeyboardEvent) => void;
|
|
57
59
|
/** Spread onto the trigger <button>. */
|
|
58
60
|
triggerProps: () => {
|
|
61
|
+
'aria-readonly': true | undefined;
|
|
59
62
|
disabled: boolean;
|
|
60
63
|
onclick: () => void;
|
|
61
64
|
onkeydown: (e: KeyboardEvent) => void;
|
|
@@ -27,6 +27,7 @@ export function createDropdownList(config) {
|
|
|
27
27
|
const optionId = (index) => `${listId}-opt-${index}`;
|
|
28
28
|
const opts = () => config.options();
|
|
29
29
|
const disabled = () => config.disabled?.() ?? false;
|
|
30
|
+
const readonly = () => config.readonly?.() ?? false;
|
|
30
31
|
const ariaState = () => ({
|
|
31
32
|
id: config.id?.(),
|
|
32
33
|
invalid: config.invalid?.(),
|
|
@@ -40,7 +41,7 @@ export function createDropdownList(config) {
|
|
|
40
41
|
const selected = $derived(opts().find((o) => o.value === config.value()) ?? null);
|
|
41
42
|
const enabledIdx = $derived(opts().map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0));
|
|
42
43
|
function openPanel() {
|
|
43
|
-
if (disabled() || open)
|
|
44
|
+
if (disabled() || readonly() || open)
|
|
44
45
|
return;
|
|
45
46
|
open = true;
|
|
46
47
|
active = Math.max(0, opts().findIndex((o) => o.value === config.value()));
|
|
@@ -53,6 +54,8 @@ export function createDropdownList(config) {
|
|
|
53
54
|
openPanel();
|
|
54
55
|
} }
|
|
55
56
|
function pick(i) {
|
|
57
|
+
if (readonly())
|
|
58
|
+
return;
|
|
56
59
|
const o = opts()[i];
|
|
57
60
|
if (!o || o.disabled)
|
|
58
61
|
return;
|
|
@@ -70,6 +73,8 @@ export function createDropdownList(config) {
|
|
|
70
73
|
// when the list is closed, matching a native <select>).
|
|
71
74
|
const typeahead = createTypeaheadBuffer();
|
|
72
75
|
function onTypeahead(char) {
|
|
76
|
+
if (readonly())
|
|
77
|
+
return;
|
|
73
78
|
const buffer = typeahead.push(char.toLowerCase());
|
|
74
79
|
const from = active >= 0 ? active : opts().findIndex((o) => o.value === config.value());
|
|
75
80
|
const next = nextTypeaheadIndex(opts(), buffer, from);
|
|
@@ -80,7 +85,7 @@ export function createDropdownList(config) {
|
|
|
80
85
|
config.onChange?.(opts()[next].value);
|
|
81
86
|
}
|
|
82
87
|
function onKeydown(e) {
|
|
83
|
-
if (disabled())
|
|
88
|
+
if (disabled() || readonly())
|
|
84
89
|
return;
|
|
85
90
|
if (isTypeaheadKey(e)) {
|
|
86
91
|
onTypeahead(e.key);
|
|
@@ -144,6 +149,7 @@ export function createDropdownList(config) {
|
|
|
144
149
|
'aria-controls': listId,
|
|
145
150
|
'aria-activedescendant': open && opts()[active] ? optionId(active) : undefined,
|
|
146
151
|
...editorAria(ariaState()),
|
|
152
|
+
'aria-readonly': readonly() || undefined,
|
|
147
153
|
disabled: disabled(),
|
|
148
154
|
onclick: toggle,
|
|
149
155
|
onkeydown: onKeydown,
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
import { type FormEntry } from './form-field';
|
|
23
|
+
/** Localizable strings the form generates itself (override via `messages`). */
|
|
24
|
+
export type FormMessages = {
|
|
25
|
+
required: (label: string) => string;
|
|
26
|
+
minItems: (label: string, n: number) => string;
|
|
27
|
+
maxItems: (label: string, n: number) => string;
|
|
28
|
+
/** Shown by SvForm while an async validator runs. */
|
|
29
|
+
checking: string;
|
|
30
|
+
};
|
|
23
31
|
export type FormConfig = {
|
|
24
32
|
/** The schema - a mix of flat fields and titled sections. */
|
|
25
33
|
fields: () => ReadonlyArray<FormEntry>;
|
|
@@ -29,6 +37,8 @@ export type FormConfig = {
|
|
|
29
37
|
* `submitting` stays true until the returned promise settles. */
|
|
30
38
|
onSubmit?: (values: Record<string, any>) => void | Promise<void>;
|
|
31
39
|
onChange?: (values: Record<string, any>) => void;
|
|
40
|
+
/** Override the built-in generated strings (required / min-max items / checking). */
|
|
41
|
+
messages?: Partial<FormMessages>;
|
|
32
42
|
};
|
|
33
43
|
export declare function createForm(config: FormConfig): {
|
|
34
44
|
readonly values: Record<string, any>;
|
|
@@ -58,5 +68,13 @@ export declare function createForm(config: FormConfig): {
|
|
|
58
68
|
submit: () => Promise<boolean>;
|
|
59
69
|
reset: (next?: Record<string, any>) => void;
|
|
60
70
|
setErrors: (map: Record<string, string>) => void;
|
|
71
|
+
errorList: () => {
|
|
72
|
+
name: string;
|
|
73
|
+
label: string;
|
|
74
|
+
message: string;
|
|
75
|
+
}[];
|
|
76
|
+
firstErrorField: () => string | undefined;
|
|
77
|
+
/** The resolved message strings (defaults merged with `config.messages`). */
|
|
78
|
+
messages: FormMessages;
|
|
61
79
|
};
|
|
62
80
|
export type Form = ReturnType<typeof createForm>;
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { flattenFields } from './form-field';
|
|
23
23
|
import { runRules } from './validators';
|
|
24
|
+
const DEFAULT_FORM_MESSAGES = {
|
|
25
|
+
required: (l) => `${l} is required`,
|
|
26
|
+
minItems: (_l, n) => `Add at least ${n} item(s)`,
|
|
27
|
+
maxItems: (_l, n) => `At most ${n} item(s)`,
|
|
28
|
+
checking: 'Checking…',
|
|
29
|
+
};
|
|
24
30
|
/** Deep-ish equality for dirty tracking: identity for primitives, structural
|
|
25
31
|
* (JSON) for arrays/objects. Good enough for form values. */
|
|
26
32
|
function eq(a, b) {
|
|
@@ -43,6 +49,7 @@ function resolveFlag(flag, dflt, values) {
|
|
|
43
49
|
return typeof flag === 'function' ? !!flag(values) : !!flag;
|
|
44
50
|
}
|
|
45
51
|
export function createForm(config) {
|
|
52
|
+
const M = { ...DEFAULT_FORM_MESSAGES, ...(config.messages ?? {}) };
|
|
46
53
|
let values = $state({ ...(config.initial ?? {}) });
|
|
47
54
|
let errors = $state({});
|
|
48
55
|
let touched = $state({});
|
|
@@ -73,7 +80,7 @@ export function createForm(config) {
|
|
|
73
80
|
* rows (so within-row cross-field rules work). */
|
|
74
81
|
function syncError(field, v, ctx = values) {
|
|
75
82
|
if (field.required && (v == null || v === '' || (Array.isArray(v) && !v.length)))
|
|
76
|
-
return
|
|
83
|
+
return M.required(field.label);
|
|
77
84
|
if (field.rules) {
|
|
78
85
|
const e = runRules(v, field.rules, ctx);
|
|
79
86
|
if (e)
|
|
@@ -132,6 +139,11 @@ export function createForm(config) {
|
|
|
132
139
|
const field = fieldOf(name);
|
|
133
140
|
if (!field)
|
|
134
141
|
return true;
|
|
142
|
+
// Computed fields are derived + read-only - never validated, never block submit.
|
|
143
|
+
if (field.computed) {
|
|
144
|
+
setError(name, null);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
135
147
|
// Hidden fields never block submit; clear any stale error + pending check.
|
|
136
148
|
if (!isVisible(name)) {
|
|
137
149
|
setError(name, null);
|
|
@@ -213,11 +225,11 @@ export function createForm(config) {
|
|
|
213
225
|
const items = arrayItems(name);
|
|
214
226
|
let arrErr = null;
|
|
215
227
|
if (field.required && items.length === 0)
|
|
216
|
-
arrErr =
|
|
228
|
+
arrErr = M.required(field.label);
|
|
217
229
|
else if (field.minItems != null && items.length < field.minItems)
|
|
218
|
-
arrErr =
|
|
230
|
+
arrErr = M.minItems(field.label, field.minItems);
|
|
219
231
|
else if (field.maxItems != null && items.length > field.maxItems)
|
|
220
|
-
arrErr =
|
|
232
|
+
arrErr = M.maxItems(field.label, field.maxItems);
|
|
221
233
|
setError(name, arrErr);
|
|
222
234
|
let ok = !arrErr;
|
|
223
235
|
clearNamespacedErrors(name); // rebuild fresh per-item errors
|
|
@@ -227,8 +239,22 @@ export function createForm(config) {
|
|
|
227
239
|
ok = false;
|
|
228
240
|
return ok;
|
|
229
241
|
}
|
|
242
|
+
/** True when `field` depends on `name` (cascading). */
|
|
243
|
+
function dependsOnName(field, name) {
|
|
244
|
+
const dep = field.dependsOn;
|
|
245
|
+
return dep == null ? false : Array.isArray(dep) ? dep.includes(name) : dep === name;
|
|
246
|
+
}
|
|
230
247
|
function setValue(name, v) {
|
|
231
|
-
|
|
248
|
+
let next = { ...values, [name]: v };
|
|
249
|
+
// Cascading: clear any fields that depend on this one so a stale child
|
|
250
|
+
// selection (e.g. a city under the old country) does not linger.
|
|
251
|
+
for (const f of flat()) {
|
|
252
|
+
if (dependsOnName(f, name) && next[f.name] != null) {
|
|
253
|
+
next = { ...next, [f.name]: null };
|
|
254
|
+
setError(f.name, null);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
values = next;
|
|
232
258
|
config.onChange?.(values);
|
|
233
259
|
// Re-validate live once the field has been blurred at least once.
|
|
234
260
|
if (touched[name])
|
|
@@ -270,6 +296,10 @@ export function createForm(config) {
|
|
|
270
296
|
for (const f of flat())
|
|
271
297
|
if (!isVisible(f.name))
|
|
272
298
|
delete payload[f.name];
|
|
299
|
+
// Inject computed (derived) values so the payload carries them.
|
|
300
|
+
for (const f of flat())
|
|
301
|
+
if (f.computed && isVisible(f.name))
|
|
302
|
+
payload[f.name] = f.computed(values);
|
|
273
303
|
submitting = true;
|
|
274
304
|
try {
|
|
275
305
|
await config.onSubmit?.(payload);
|
|
@@ -335,6 +365,17 @@ export function createForm(config) {
|
|
|
335
365
|
touched = nextTouched;
|
|
336
366
|
}
|
|
337
367
|
const isFieldDirty = (name) => !eq(values[name], baseline[name]);
|
|
368
|
+
/** Top-level field errors (visible fields only), for a form-level summary. */
|
|
369
|
+
function errorList() {
|
|
370
|
+
const out = [];
|
|
371
|
+
for (const f of flat()) {
|
|
372
|
+
const m = errors[f.name];
|
|
373
|
+
if (m && isVisible(f.name))
|
|
374
|
+
out.push({ name: f.name, label: f.label, message: m });
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
const firstErrorField = () => errorList()[0]?.name;
|
|
338
379
|
return {
|
|
339
380
|
get values() { return values; },
|
|
340
381
|
get submitting() { return submitting; },
|
|
@@ -345,7 +386,7 @@ export function createForm(config) {
|
|
|
345
386
|
return true;
|
|
346
387
|
return false;
|
|
347
388
|
},
|
|
348
|
-
value: (name) => values[name],
|
|
389
|
+
value: (name) => { const f = fieldOf(name); return f?.computed ? f.computed(values) : values[name]; },
|
|
349
390
|
error: (name) => errors[name],
|
|
350
391
|
isTouched: (name) => !!touched[name],
|
|
351
392
|
hasError: (name) => !!errors[name],
|
|
@@ -370,5 +411,9 @@ export function createForm(config) {
|
|
|
370
411
|
submit,
|
|
371
412
|
reset,
|
|
372
413
|
setErrors,
|
|
414
|
+
errorList,
|
|
415
|
+
firstErrorField,
|
|
416
|
+
/** The resolved message strings (defaults merged with `config.messages`). */
|
|
417
|
+
messages: M,
|
|
373
418
|
};
|
|
374
419
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { enabledIndices, wrapMove } from './list-nav';
|
|
2
|
+
import { createTypeaheadBuffer, isTypeaheadKey } from './list-option';
|
|
2
3
|
let uid = 0;
|
|
3
4
|
export function createMenu(config) {
|
|
4
5
|
const items = () => config.items();
|
|
@@ -7,6 +8,23 @@ export function createMenu(config) {
|
|
|
7
8
|
// -1 = nothing focused yet (the first enabled item is the tab stop).
|
|
8
9
|
let active = $state(-1);
|
|
9
10
|
let openSub = $state(-1);
|
|
11
|
+
// Type-ahead: printable keys jump to the next item whose label starts with the
|
|
12
|
+
// accumulated buffer (WAI-ARIA menu behavior), wrapping past the active item.
|
|
13
|
+
const typeahead = createTypeaheadBuffer();
|
|
14
|
+
function typeaheadMatch(buffer) {
|
|
15
|
+
const b = buffer.trim().toLowerCase();
|
|
16
|
+
if (!b)
|
|
17
|
+
return -1;
|
|
18
|
+
const its = items();
|
|
19
|
+
const n = its.length;
|
|
20
|
+
for (let k = 1; k <= n; k++) {
|
|
21
|
+
const i = (active + k + n) % n;
|
|
22
|
+
const it = its[i];
|
|
23
|
+
if (it && !it.disabled && !it.separator && (it.label ?? '').toLowerCase().startsWith(b))
|
|
24
|
+
return i;
|
|
25
|
+
}
|
|
26
|
+
return -1;
|
|
27
|
+
}
|
|
10
28
|
// Real (non-separator, non-disabled) item indices, for roving focus.
|
|
11
29
|
const focusables = $derived(enabledIndices(items().length, (i) => {
|
|
12
30
|
const it = items()[i];
|
|
@@ -108,6 +126,15 @@ export function createMenu(config) {
|
|
|
108
126
|
case 'Tab':
|
|
109
127
|
config.onClose();
|
|
110
128
|
break;
|
|
129
|
+
default:
|
|
130
|
+
// Type-ahead: a bare printable key jumps to the matching item.
|
|
131
|
+
if (isTypeaheadKey(e)) {
|
|
132
|
+
const idx = typeaheadMatch(typeahead.push(e.key));
|
|
133
|
+
if (idx >= 0) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
focus(idx);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
111
138
|
}
|
|
112
139
|
}
|
|
113
140
|
function itemTabIndex(i) {
|
|
@@ -18,6 +18,8 @@ export type PopoverSelectConfig = {
|
|
|
18
18
|
itemCount: () => number;
|
|
19
19
|
/** Whether item `i` is disabled (skipped by roving, not selectable). */
|
|
20
20
|
disabled?: (index: number) => boolean;
|
|
21
|
+
/** Read-only: value shown, trigger focusable, but the panel will not open. */
|
|
22
|
+
readonly?: () => boolean;
|
|
21
23
|
/** Commit the item at `index` (Enter or a click the component forwards). */
|
|
22
24
|
onSelect: (index: number) => void;
|
|
23
25
|
/** Notified whenever the open state changes. */
|
|
@@ -69,6 +71,7 @@ export declare function createPopoverSelect(config: PopoverSelectConfig): {
|
|
|
69
71
|
'aria-haspopup': "listbox" | "grid" | "tree";
|
|
70
72
|
'aria-expanded': boolean;
|
|
71
73
|
'aria-controls': string | undefined;
|
|
74
|
+
'aria-readonly': true | undefined;
|
|
72
75
|
onkeydown: (e: KeyboardEvent) => void;
|
|
73
76
|
};
|
|
74
77
|
/** Attributes for the element that OWNS focus + activedescendant (the panel,
|
|
@@ -25,6 +25,7 @@ export function createPopoverSelect(config) {
|
|
|
25
25
|
let rect = $state({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 });
|
|
26
26
|
const count = () => config.itemCount();
|
|
27
27
|
const isDisabled = (i) => config.disabled?.(i) ?? false;
|
|
28
|
+
const readonly = () => config.readonly?.() ?? false;
|
|
28
29
|
function enabledIndices() {
|
|
29
30
|
return enabledIndicesOf(count(), isDisabled);
|
|
30
31
|
}
|
|
@@ -39,6 +40,8 @@ export function createPopoverSelect(config) {
|
|
|
39
40
|
config.onOpenChange?.(v);
|
|
40
41
|
}
|
|
41
42
|
function openPanel() {
|
|
43
|
+
if (readonly())
|
|
44
|
+
return;
|
|
42
45
|
active = clampActive(config.initialActive?.() ?? 0);
|
|
43
46
|
setOpen(true);
|
|
44
47
|
}
|
|
@@ -47,6 +50,8 @@ export function createPopoverSelect(config) {
|
|
|
47
50
|
config.getTrigger()?.focus();
|
|
48
51
|
}
|
|
49
52
|
function toggle() {
|
|
53
|
+
if (readonly())
|
|
54
|
+
return;
|
|
50
55
|
open ? setOpen(false) : openPanel();
|
|
51
56
|
}
|
|
52
57
|
function move(delta) {
|
|
@@ -57,7 +62,7 @@ export function createPopoverSelect(config) {
|
|
|
57
62
|
function first() { active = enabledIndices()[0] ?? 0; }
|
|
58
63
|
function last() { active = enabledIndices().at(-1) ?? 0; }
|
|
59
64
|
function selectActive() {
|
|
60
|
-
if (count() === 0 || isDisabled(active))
|
|
65
|
+
if (readonly() || count() === 0 || isDisabled(active))
|
|
61
66
|
return;
|
|
62
67
|
config.onSelect(active);
|
|
63
68
|
if (config.closeOnSelect?.() ?? true)
|
|
@@ -95,6 +100,8 @@ export function createPopoverSelect(config) {
|
|
|
95
100
|
};
|
|
96
101
|
});
|
|
97
102
|
function onTriggerKeydown(e) {
|
|
103
|
+
if (readonly())
|
|
104
|
+
return;
|
|
98
105
|
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
99
106
|
e.preventDefault();
|
|
100
107
|
openPanel();
|
|
@@ -172,6 +179,7 @@ export function createPopoverSelect(config) {
|
|
|
172
179
|
'aria-haspopup': haspopup,
|
|
173
180
|
'aria-expanded': open,
|
|
174
181
|
'aria-controls': open ? panelId : undefined,
|
|
182
|
+
'aria-readonly': readonly() || undefined,
|
|
175
183
|
onkeydown: onTriggerKeydown,
|
|
176
184
|
}),
|
|
177
185
|
/** Attributes for the element that OWNS focus + activedescendant (the panel,
|
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* createSlider - the HEADLESS core behind <SvSlider>, in the same spirit as
|
|
3
|
-
* `createSvGrid` for the grid: a runes-based state machine (single + dual-thumb,
|
|
4
|
-
* stepping, keyboard, drag state) that owns the value<->position math and the
|
|
5
|
-
* ARIA wiring, exposed as **prop-getters** you spread onto YOUR OWN markup.
|
|
6
|
-
*
|
|
7
|
-
* The core never touches the DOM: the styled renderer measures the track
|
|
8
|
-
* (`getBoundingClientRect`) and passes the rect + pointer position IN via
|
|
9
|
-
* `pointerDown` / `setFromPointer`. The core turns that into a stepped value.
|
|
10
|
-
*
|
|
11
|
-
* ```svelte
|
|
12
|
-
* <script lang="ts">
|
|
13
|
-
* import { createSlider } from '@svgrid/grid'
|
|
14
|
-
* let value = $state(50)
|
|
15
|
-
* const s = createSlider({ value: () => value, onChange: (v) => (value = v as number) })
|
|
16
|
-
* </script>
|
|
17
|
-
* <div bind:this={track} onpointerdown={(e) => s.pointerDown({ x: e.clientX, y: e.clientY }, track.getBoundingClientRect())}>
|
|
18
|
-
* <div {...s.thumbProps(0)} style:left={s.percentOf(s.hi) + '%'}></div>
|
|
19
|
-
* </div>
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
1
|
export type SliderValue = number | [number, number];
|
|
23
2
|
export type SliderOrientation = 'horizontal' | 'vertical';
|
|
24
3
|
export type SliderThumb = 'single' | 'lo' | 'hi';
|
|
@@ -39,6 +18,11 @@ export type SliderConfig = {
|
|
|
39
18
|
ariaLabel?: () => string | undefined;
|
|
40
19
|
/** Right-to-left: mirrors horizontal pointer + Left/Right key direction. */
|
|
41
20
|
rtl?: () => boolean;
|
|
21
|
+
id?: () => string | undefined;
|
|
22
|
+
invalid?: () => boolean | undefined;
|
|
23
|
+
required?: () => boolean | undefined;
|
|
24
|
+
error?: () => string | undefined;
|
|
25
|
+
hint?: () => string | undefined;
|
|
42
26
|
};
|
|
43
27
|
export declare function createSlider(config: SliderConfig): {
|
|
44
28
|
readonly value: SliderValue;
|
|
@@ -67,6 +51,9 @@ export declare function createSlider(config: SliderConfig): {
|
|
|
67
51
|
'aria-label': string;
|
|
68
52
|
'aria-orientation': SliderOrientation;
|
|
69
53
|
'aria-disabled': true | undefined;
|
|
54
|
+
'aria-invalid': "true" | undefined;
|
|
55
|
+
'aria-required': "true" | undefined;
|
|
56
|
+
'aria-describedby': string | undefined;
|
|
70
57
|
onkeydown: (e: KeyboardEvent) => void;
|
|
71
58
|
};
|
|
72
59
|
};
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createSlider - the HEADLESS core behind <SvSlider>, in the same spirit as
|
|
3
|
+
* `createSvGrid` for the grid: a runes-based state machine (single + dual-thumb,
|
|
4
|
+
* stepping, keyboard, drag state) that owns the value<->position math and the
|
|
5
|
+
* ARIA wiring, exposed as **prop-getters** you spread onto YOUR OWN markup.
|
|
6
|
+
*
|
|
7
|
+
* The core never touches the DOM: the styled renderer measures the track
|
|
8
|
+
* (`getBoundingClientRect`) and passes the rect + pointer position IN via
|
|
9
|
+
* `pointerDown` / `setFromPointer`. The core turns that into a stepped value.
|
|
10
|
+
*
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <script lang="ts">
|
|
13
|
+
* import { createSlider } from '@svgrid/grid'
|
|
14
|
+
* let value = $state(50)
|
|
15
|
+
* const s = createSlider({ value: () => value, onChange: (v) => (value = v as number) })
|
|
16
|
+
* </script>
|
|
17
|
+
* <div bind:this={track} onpointerdown={(e) => s.pointerDown({ x: e.clientX, y: e.clientY }, track.getBoundingClientRect())}>
|
|
18
|
+
* <div {...s.thumbProps(0)} style:left={s.percentOf(s.hi) + '%'}></div>
|
|
19
|
+
* </div>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import { editorAria } from './editor-contract';
|
|
1
23
|
export function createSlider(config) {
|
|
2
24
|
const min = () => config.min?.() ?? 0;
|
|
3
25
|
const max = () => config.max?.() ?? 100;
|
|
@@ -118,6 +140,15 @@ export function createSlider(config) {
|
|
|
118
140
|
const vmin = which === 'hi' ? lo : min();
|
|
119
141
|
const vmax = which === 'lo' ? hi : max();
|
|
120
142
|
const label = which === 'lo' ? 'Minimum' : which === 'hi' ? 'Maximum' : (config.ariaLabel?.() ?? 'Value');
|
|
143
|
+
// Validation ARIA (invalid / required / describedby) from the editor
|
|
144
|
+
// contract; keep the thumb's own value-oriented aria-label.
|
|
145
|
+
const va = editorAria({
|
|
146
|
+
id: config.id?.(),
|
|
147
|
+
invalid: config.invalid?.(),
|
|
148
|
+
required: config.required?.(),
|
|
149
|
+
error: config.error?.(),
|
|
150
|
+
hint: config.hint?.(),
|
|
151
|
+
});
|
|
121
152
|
return {
|
|
122
153
|
role: 'slider',
|
|
123
154
|
tabindex: disabled() ? -1 : 0,
|
|
@@ -127,6 +158,9 @@ export function createSlider(config) {
|
|
|
127
158
|
'aria-label': label,
|
|
128
159
|
'aria-orientation': (isVert() ? 'vertical' : 'horizontal'),
|
|
129
160
|
'aria-disabled': disabled() || undefined,
|
|
161
|
+
'aria-invalid': va['aria-invalid'],
|
|
162
|
+
'aria-required': va['aria-required'],
|
|
163
|
+
'aria-describedby': va['aria-describedby'],
|
|
130
164
|
onkeydown: (e) => onThumbKey(e, which),
|
|
131
165
|
};
|
|
132
166
|
},
|