@proyecto-viviana/solid-stately 0.6.0-rc.0 → 0.6.0-rc.1
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 +59 -29
- package/dist/calendar/createCalendarState.d.ts +3 -1
- package/dist/calendar/createCalendarState.d.ts.map +1 -1
- package/dist/calendar/createDatePickerState.d.ts +2 -0
- package/dist/calendar/createDatePickerState.d.ts.map +1 -1
- package/dist/calendar/createRangeCalendarState.d.ts +2 -0
- package/dist/calendar/createRangeCalendarState.d.ts.map +1 -1
- package/dist/calendar/createTimeFieldState.d.ts +1 -1
- package/dist/calendar/createTimeFieldState.d.ts.map +1 -1
- package/dist/checkbox/createCheckboxGroupState.d.ts +2 -0
- package/dist/checkbox/createCheckboxGroupState.d.ts.map +1 -1
- package/dist/collections/createListState.d.ts.map +1 -1
- package/dist/collections/createMenuState.d.ts +46 -2
- package/dist/collections/createMenuState.d.ts.map +1 -1
- package/dist/collections/createSelectionState.d.ts.map +1 -1
- package/dist/collections/index.d.ts +1 -1
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/color/createColorAreaState.d.ts +4 -0
- package/dist/color/createColorAreaState.d.ts.map +1 -1
- package/dist/color/createColorFieldState.d.ts +2 -0
- package/dist/color/createColorFieldState.d.ts.map +1 -1
- package/dist/color/createColorSliderState.d.ts +2 -0
- package/dist/color/createColorSliderState.d.ts.map +1 -1
- package/dist/color/createColorWheelState.d.ts.map +1 -1
- package/dist/combobox/createComboBoxState.d.ts +90 -23
- package/dist/combobox/createComboBoxState.d.ts.map +1 -1
- package/dist/combobox/index.d.ts +1 -1
- package/dist/combobox/index.d.ts.map +1 -1
- package/dist/data/createAsyncList.d.ts.map +1 -1
- package/dist/data/createListData.d.ts.map +1 -1
- package/dist/data/createTreeData.d.ts.map +1 -1
- package/dist/dnd/createDraggableCollectionState.d.ts +5 -1
- package/dist/dnd/createDraggableCollectionState.d.ts.map +1 -1
- package/dist/dnd/createDroppableCollectionState.d.ts +6 -0
- package/dist/dnd/createDroppableCollectionState.d.ts.map +1 -1
- package/dist/form/createFormValidationState.d.ts.map +1 -1
- package/dist/grid/createGridState.d.ts +2 -2
- package/dist/grid/createGridState.d.ts.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1012 -471
- package/dist/index.js.map +1 -1
- package/dist/menu/index.d.ts +2 -1
- package/dist/menu/index.d.ts.map +1 -1
- package/dist/numberfield/createNumberFieldState.d.ts +6 -3
- package/dist/numberfield/createNumberFieldState.d.ts.map +1 -1
- package/dist/private/flags/flags.d.ts.map +1 -0
- package/dist/private/flags/flags.js +1 -1
- package/dist/private/flags/flags.js.map +1 -1
- package/dist/radio/createRadioGroupState.d.ts.map +1 -1
- package/dist/select/createSelectState.d.ts +85 -29
- package/dist/select/createSelectState.d.ts.map +1 -1
- package/dist/select/index.d.ts +1 -1
- package/dist/select/index.d.ts.map +1 -1
- package/dist/selection/SelectionManager.d.ts.map +1 -1
- package/dist/selection/createMultipleSelectionState.d.ts.map +1 -1
- package/dist/slider/createSliderState.d.ts +35 -9
- package/dist/slider/createSliderState.d.ts.map +1 -1
- package/dist/ssr/index.d.ts +0 -6
- package/dist/ssr/index.d.ts.map +1 -1
- package/dist/steplist/createStepListState.d.ts +8 -13
- package/dist/steplist/createStepListState.d.ts.map +1 -1
- package/dist/table/createTableColumnResizeState.d.ts.map +1 -1
- package/dist/table/createTableState.d.ts +2 -2
- package/dist/table/createTableState.d.ts.map +1 -1
- package/dist/table/createTreeGridState.d.ts +2 -2
- package/dist/table/createTreeGridState.d.ts.map +1 -1
- package/dist/table/types.d.ts +2 -2
- package/dist/table/types.d.ts.map +1 -1
- package/dist/tabs/createTabListState.d.ts.map +1 -1
- package/dist/tokenfield/TokenFieldValue.d.ts +28 -4
- package/dist/tokenfield/TokenFieldValue.d.ts.map +1 -1
- package/dist/tokenfield/index.d.ts +1 -1
- package/dist/tokenfield/index.d.ts.map +1 -1
- package/dist/tree/createTreeState.d.ts +2 -2
- package/dist/tree/createTreeState.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/reactivity.d.ts +5 -0
- package/dist/utils/reactivity.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/calendar/createCalendarState.ts +139 -58
- package/src/calendar/createDatePickerState.ts +14 -11
- package/src/calendar/createRangeCalendarState.ts +43 -17
- package/src/calendar/createTimeFieldState.ts +1 -1
- package/src/checkbox/createCheckboxGroupState.ts +16 -0
- package/src/collections/createListState.ts +4 -4
- package/src/collections/createMenuState.ts +134 -4
- package/src/collections/createSelectionState.ts +2 -2
- package/src/collections/index.ts +4 -0
- package/src/color/createColorAreaState.ts +19 -0
- package/src/color/createColorFieldState.ts +76 -26
- package/src/color/createColorSliderState.ts +25 -2
- package/src/color/createColorWheelState.ts +9 -5
- package/src/combobox/createComboBoxState.ts +371 -121
- package/src/combobox/index.ts +4 -0
- package/src/data/createAsyncList.ts +19 -4
- package/src/data/createListData.ts +8 -2
- package/src/data/createTreeData.ts +11 -3
- package/src/dnd/createDraggableCollectionState.ts +8 -1
- package/src/dnd/createDroppableCollectionState.ts +10 -0
- package/src/form/createFormValidationState.ts +17 -3
- package/src/grid/createGridState.ts +15 -8
- package/src/index.ts +18 -2
- package/src/menu/index.ts +5 -0
- package/src/numberfield/createNumberFieldState.ts +97 -35
- package/src/radio/createRadioGroupState.ts +4 -1
- package/src/select/createSelectState.ts +253 -131
- package/src/select/index.ts +8 -1
- package/src/selection/SelectionManager.ts +3 -1
- package/src/selection/createMultipleSelectionState.ts +2 -2
- package/src/slider/createSliderState.ts +227 -103
- package/src/ssr/index.ts +11 -4
- package/src/steplist/createStepListState.ts +156 -123
- package/src/table/createTableColumnResizeState.ts +51 -11
- package/src/table/createTableState.ts +5 -4
- package/src/table/createTreeGridState.ts +3 -4
- package/src/table/types.ts +2 -2
- package/src/tabs/createTabListState.ts +8 -31
- package/src/tokenfield/TokenFieldValue.ts +75 -16
- package/src/tokenfield/index.ts +1 -0
- package/src/tree/createTreeState.ts +15 -8
- package/src/utils/index.ts +1 -0
- package/src/utils/reactivity.ts +8 -0
- package/dist/flags/flags.d.ts.map +0 -1
- /package/dist/{flags → private/flags}/flags.d.ts +0 -0
- /package/src/{flags → private/flags}/flags.ts +0 -0
|
@@ -107,6 +107,8 @@ export interface RangeCalendarStateProps<T extends DateValue = DateValue> {
|
|
|
107
107
|
isDateDisabled?: (date: DateValue) => boolean;
|
|
108
108
|
/** Validation state. */
|
|
109
109
|
validationState?: MaybeAccessor<ValidationState | undefined>;
|
|
110
|
+
/** Whether the current selection is invalid according to application logic. */
|
|
111
|
+
isInvalid?: boolean;
|
|
110
112
|
/** Whether to allow selecting the same date for start and end. */
|
|
111
113
|
allowsNonContiguousRanges?: boolean;
|
|
112
114
|
/** The first day of the week (0 = Sunday, 1 = Monday, etc.). */
|
|
@@ -315,10 +317,14 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
315
317
|
|
|
316
318
|
const rawAlignStart = (date: CalendarDate): CalendarDate => startOfMonth(date);
|
|
317
319
|
|
|
318
|
-
const rawAlignEnd = (date: CalendarDate): CalendarDate =>
|
|
319
|
-
startOfMonth(date).subtract({ months: Math.max(
|
|
320
|
+
const rawAlignEnd = (date: CalendarDate, months = visibleMonths()): CalendarDate =>
|
|
321
|
+
startOfMonth(date).subtract({ months: Math.max(months - 1, 0) });
|
|
320
322
|
|
|
321
|
-
const constrainVisibleRangeStart = (
|
|
323
|
+
const constrainVisibleRangeStart = (
|
|
324
|
+
date: CalendarDate,
|
|
325
|
+
aligned: CalendarDate,
|
|
326
|
+
months = visibleMonths(),
|
|
327
|
+
): CalendarDate => {
|
|
322
328
|
const minValue = access(props.minValue);
|
|
323
329
|
const maxValue = access(props.maxValue);
|
|
324
330
|
let constrained = aligned;
|
|
@@ -331,7 +337,7 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
331
337
|
}
|
|
332
338
|
|
|
333
339
|
if (maxValue && date.compare(toDisplayCalendarDate(maxValue)) <= 0) {
|
|
334
|
-
const maxStart = rawAlignEnd(toDisplayCalendarDate(maxValue));
|
|
340
|
+
const maxStart = rawAlignEnd(toDisplayCalendarDate(maxValue), months);
|
|
335
341
|
if (maxStart.compare(constrained) < 0) {
|
|
336
342
|
constrained = maxStart;
|
|
337
343
|
}
|
|
@@ -355,8 +361,11 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
355
361
|
return constrainVisibleRangeStart(date, rawAlignStart(date).subtract({ months: offset }));
|
|
356
362
|
};
|
|
357
363
|
|
|
364
|
+
// A controlled null is a cleared selection. Pin useControlledState treats
|
|
365
|
+
// `value !== undefined` as controlled, so null must not fall through to defaultValue.
|
|
358
366
|
const defaultSelectionAlignment = (): RangeCalendarSelectionAlignment => {
|
|
359
|
-
const
|
|
367
|
+
const controlled = access(props.value);
|
|
368
|
+
const currentValue = controlled !== undefined ? controlled : props.defaultValue;
|
|
360
369
|
if (!currentValue?.start || !currentValue?.end) {
|
|
361
370
|
return "center";
|
|
362
371
|
}
|
|
@@ -388,10 +397,11 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
388
397
|
return toDisplayCalendarDate(props.defaultFocusedValue);
|
|
389
398
|
}
|
|
390
399
|
const controlledValue = access(props.value);
|
|
391
|
-
if (controlledValue
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
400
|
+
if (controlledValue !== undefined) {
|
|
401
|
+
if (controlledValue?.start) {
|
|
402
|
+
return toDisplayCalendarDate(controlledValue.start);
|
|
403
|
+
}
|
|
404
|
+
} else if (props.defaultValue?.start) {
|
|
395
405
|
return toDisplayCalendarDate(props.defaultValue.start);
|
|
396
406
|
}
|
|
397
407
|
return intlToCalendar(today(timeZone), calendar());
|
|
@@ -451,7 +461,10 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
451
461
|
isDateOutsideRange(toDisplayCalendarDate(v.end), minCal, maxCal)
|
|
452
462
|
);
|
|
453
463
|
});
|
|
454
|
-
const isValueInvalid = createMemo(
|
|
464
|
+
const isValueInvalid = createMemo(
|
|
465
|
+
() =>
|
|
466
|
+
Boolean(access(props.isInvalid)) || validationState() === "invalid" || isInvalidSelection(),
|
|
467
|
+
);
|
|
455
468
|
|
|
456
469
|
// Highlighted range during selection
|
|
457
470
|
const highlightedRange = createMemo<RangeValue<CalendarDate> | null>(() => {
|
|
@@ -736,17 +749,21 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
736
749
|
// advance the grid.
|
|
737
750
|
const focusPreviousPage = () => {
|
|
738
751
|
const pageMonths = access(props.pageBehavior) === "single" ? 1 : visibleMonths();
|
|
739
|
-
const
|
|
752
|
+
const currentFocused = focusedDate();
|
|
753
|
+
const nextFocusedDate = constrainDate(currentFocused.subtract({ months: pageMonths }));
|
|
754
|
+
const slidStart = startOfMonth(visibleRangeStart().subtract({ months: pageMonths }));
|
|
740
755
|
setFocusedDateInternal(nextFocusedDate);
|
|
741
|
-
setVisibleRangeStart(
|
|
756
|
+
setVisibleRangeStart(constrainVisibleRangeStart(currentFocused, slidStart, pageMonths));
|
|
742
757
|
props.onFocusChange?.(nextFocusedDate);
|
|
743
758
|
};
|
|
744
759
|
|
|
745
760
|
const focusNextPage = () => {
|
|
746
761
|
const pageMonths = access(props.pageBehavior) === "single" ? 1 : visibleMonths();
|
|
747
|
-
const
|
|
762
|
+
const currentFocused = focusedDate();
|
|
763
|
+
const nextFocusedDate = constrainDate(currentFocused.add({ months: pageMonths }));
|
|
764
|
+
const slidStart = startOfMonth(visibleRangeStart().add({ months: pageMonths }));
|
|
748
765
|
setFocusedDateInternal(nextFocusedDate);
|
|
749
|
-
setVisibleRangeStart(
|
|
766
|
+
setVisibleRangeStart(constrainVisibleRangeStart(currentFocused, slidStart, pageMonths));
|
|
750
767
|
props.onFocusChange?.(nextFocusedDate);
|
|
751
768
|
};
|
|
752
769
|
|
|
@@ -881,9 +898,18 @@ export function createRangeCalendarState<T extends DateValue = CalendarDate>(
|
|
|
881
898
|
};
|
|
882
899
|
|
|
883
900
|
const focusNearestAvailableDate = (anchor: CalendarDate) => {
|
|
884
|
-
// RAC `useRangeCalendarState.ts:261-273`: prefer anchor+1, else −1
|
|
885
|
-
|
|
886
|
-
|
|
901
|
+
// RAC `useRangeCalendarState.ts:261-273`: prefer anchor+1, else −1.
|
|
902
|
+
// Invalid is outside min/max or this anchor's contiguous span. A day in
|
|
903
|
+
// the next month, or an unavailable day when non-contiguous ranges are
|
|
904
|
+
// allowed, is still a valid focus target.
|
|
905
|
+
const passedRange = getAvailableRange(anchor);
|
|
906
|
+
const isDateInvalid = (candidate: CalendarDate) => {
|
|
907
|
+
if (isDateOutsideAllowedRange(candidate)) return true;
|
|
908
|
+
const calDate = toDisplayCalendarDate(candidate);
|
|
909
|
+
if (passedRange?.start && calDate.compare(passedRange.start) < 0) return true;
|
|
910
|
+
if (passedRange?.end && calDate.compare(passedRange.end) > 0) return true;
|
|
911
|
+
return false;
|
|
912
|
+
};
|
|
887
913
|
let nextDay = anchor.add({ days: 1 });
|
|
888
914
|
if (isDateInvalid(nextDay)) {
|
|
889
915
|
nextDay = anchor.subtract({ days: 1 });
|
|
@@ -122,7 +122,7 @@ export interface TimeFieldStateProps<T extends TimeValue = Time> {
|
|
|
122
122
|
* consumers.
|
|
123
123
|
*/
|
|
124
124
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
125
|
-
export interface TimeFieldState<
|
|
125
|
+
export interface TimeFieldState<_T extends TimeValue = Time> extends DateFieldState {
|
|
126
126
|
/** The current value in the `Time`/`CalendarDateTime`/`ZonedDateTime` domain. */
|
|
127
127
|
timeValue: Accessor<TimeValue | null>;
|
|
128
128
|
}
|
|
@@ -25,6 +25,7 @@ import { createMemo, Accessor } from "solid-js";
|
|
|
25
25
|
import { access, createInternalSignal, readNow, type MaybeAccessor } from "../utils";
|
|
26
26
|
import {
|
|
27
27
|
createFormValidationState,
|
|
28
|
+
mergeValidation,
|
|
28
29
|
type FormValidationState,
|
|
29
30
|
type ValidationFunction,
|
|
30
31
|
type ValidationResult,
|
|
@@ -98,6 +99,8 @@ export interface CheckboxGroupState extends Pick<
|
|
|
98
99
|
removeValue(value: string): void;
|
|
99
100
|
/** Toggles a value in the set of selected values. */
|
|
100
101
|
toggleValue(value: string): void;
|
|
102
|
+
/** Records one checkbox's validation and merges it into the group. */
|
|
103
|
+
setInvalid(value: string, validation: ValidationResult): void;
|
|
101
104
|
|
|
102
105
|
/** Current display validation result for the group. */
|
|
103
106
|
readonly displayValidation: Accessor<ValidationResult>;
|
|
@@ -195,6 +198,18 @@ export function createCheckboxGroupState(
|
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
|
|
201
|
+
const invalidValues = new Map<string, ValidationResult>();
|
|
202
|
+
|
|
203
|
+
function setInvalid(itemValue: string, result: ValidationResult): void {
|
|
204
|
+
if (result.isInvalid) {
|
|
205
|
+
invalidValues.set(itemValue, result);
|
|
206
|
+
} else {
|
|
207
|
+
invalidValues.delete(itemValue);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
validation.updateValidation(mergeValidation(...invalidValues.values()));
|
|
211
|
+
}
|
|
212
|
+
|
|
198
213
|
function toggleValue(toggleVal: string): void {
|
|
199
214
|
const p = getProps();
|
|
200
215
|
if (p.isReadOnly || p.isDisabled) {
|
|
@@ -227,6 +242,7 @@ export function createCheckboxGroupState(
|
|
|
227
242
|
addValue,
|
|
228
243
|
removeValue,
|
|
229
244
|
toggleValue,
|
|
245
|
+
setInvalid,
|
|
230
246
|
realtimeValidation: validation.realtimeValidation,
|
|
231
247
|
displayValidation: validation.displayValidation,
|
|
232
248
|
updateValidation: validation.updateValidation,
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { createMemo, createEffect } from "solid-js";
|
|
25
25
|
import type { Accessor } from "solid-js";
|
|
26
|
-
import {
|
|
26
|
+
import { propsAccessor, type MaybeAccessor } from "../utils";
|
|
27
27
|
import { ListCollection } from "./ListCollection";
|
|
28
28
|
import type { SelectionState, SelectionPressEvent } from "./createSelectionState";
|
|
29
29
|
import { createMultipleSelectionState } from "../selection/createMultipleSelectionState";
|
|
@@ -91,7 +91,7 @@ export interface ListState<T = unknown> extends SelectionState {
|
|
|
91
91
|
export function createListState<T = unknown>(
|
|
92
92
|
props: MaybeAccessor<ListStateProps<T>>,
|
|
93
93
|
): ListState<T> {
|
|
94
|
-
const getProps = (
|
|
94
|
+
const getProps = propsAccessor(props);
|
|
95
95
|
|
|
96
96
|
// Build collection from items (memoized to avoid rebuilding on every access)
|
|
97
97
|
const collection: Accessor<Collection<T>> = createMemo(() => {
|
|
@@ -349,7 +349,7 @@ export interface SingleSelectListState<T = unknown> extends ListState<T> {
|
|
|
349
349
|
export function createSingleSelectListState<T = unknown>(
|
|
350
350
|
props: MaybeAccessor<SingleSelectListStateProps<T>>,
|
|
351
351
|
): SingleSelectListState<T> {
|
|
352
|
-
const getProps = (
|
|
352
|
+
const getProps = propsAccessor(props);
|
|
353
353
|
|
|
354
354
|
// Convert single selection props to multiple selection props
|
|
355
355
|
const listState = createListState<T>({
|
|
@@ -376,7 +376,7 @@ export function createSingleSelectListState<T = unknown>(
|
|
|
376
376
|
allowDuplicateSelectionEvents: true,
|
|
377
377
|
get selectedKeys() {
|
|
378
378
|
const key = getProps().selectedKey;
|
|
379
|
-
return key != null ? [key] : [];
|
|
379
|
+
return key !== undefined ? (key != null ? [key] : []) : undefined;
|
|
380
380
|
},
|
|
381
381
|
get defaultSelectedKeys() {
|
|
382
382
|
const key = getProps().defaultSelectedKey;
|
|
@@ -10,17 +10,32 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/*
|
|
14
|
+
* Copyright 2023 Adobe. All rights reserved.
|
|
15
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
16
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
17
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
18
|
+
*
|
|
19
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
20
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
21
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
22
|
+
* governing permissions and limitations under the License.
|
|
23
|
+
*/
|
|
24
|
+
|
|
13
25
|
// Ported to SolidJS for Proyecto Viviana; based on packages/react-stately/src/menu/useMenuTriggerState.ts
|
|
26
|
+
// Ported to SolidJS for Proyecto Viviana; based on packages/react-stately/src/menu/useSubmenuTriggerState.ts
|
|
14
27
|
|
|
15
28
|
/**
|
|
16
29
|
* State management for menu components.
|
|
17
30
|
*
|
|
18
31
|
* createMenuTriggerState is ported from
|
|
19
32
|
* packages/react-stately/src/menu/useMenuTriggerState.ts.
|
|
33
|
+
* createSubmenuTriggerState is ported from
|
|
34
|
+
* packages/react-stately/src/menu/useSubmenuTriggerState.ts.
|
|
20
35
|
* createMenuState is a local composition of the separately attributed list state.
|
|
21
36
|
*/
|
|
22
37
|
|
|
23
|
-
import {
|
|
38
|
+
import { createInternalSignal, propsAccessor, type MaybeAccessor } from "../utils";
|
|
24
39
|
|
|
25
40
|
import { createOverlayTriggerState, type OverlayTriggerProps } from "../overlays";
|
|
26
41
|
import { createListState, type ListState, type ListStateProps } from "./createListState";
|
|
@@ -45,7 +60,7 @@ export interface MenuState<T = unknown> extends ListState<T> {
|
|
|
45
60
|
export function createMenuState<T = unknown>(
|
|
46
61
|
props: MaybeAccessor<MenuStateProps<T>>,
|
|
47
62
|
): MenuState<T> {
|
|
48
|
-
const getProps = (
|
|
63
|
+
const getProps = propsAccessor(props);
|
|
49
64
|
|
|
50
65
|
// Menus default to action-only items, but can opt into single or multiple selection.
|
|
51
66
|
const listState = createListState<T>({
|
|
@@ -134,15 +149,51 @@ export interface MenuTriggerState {
|
|
|
134
149
|
setPoint(point: { x: number; y: number }): void;
|
|
135
150
|
}
|
|
136
151
|
|
|
152
|
+
export interface RootMenuTriggerState extends MenuTriggerState {
|
|
153
|
+
/** Opens a submenu tied to a menu item at a level. */
|
|
154
|
+
openSubmenu(triggerKey: Key, level: number): void;
|
|
155
|
+
/** Closes a submenu tied to a menu item at a level. */
|
|
156
|
+
closeSubmenu(triggerKey: Key, level: number): void;
|
|
157
|
+
/**
|
|
158
|
+
* Open submenu trigger keys. The index matches the submenu level.
|
|
159
|
+
*/
|
|
160
|
+
readonly expandedKeysStack: () => Key[];
|
|
161
|
+
}
|
|
162
|
+
|
|
137
163
|
/**
|
|
138
164
|
* Creates state for a menu trigger. Mirrors `@react-stately/menu` `useMenuTriggerState`:
|
|
139
|
-
* overlay open state
|
|
165
|
+
* overlay open state, the focus strategy passed through to `createMenu`, and the
|
|
166
|
+
* expanded submenu stack.
|
|
140
167
|
*/
|
|
141
168
|
export function createMenuTriggerState(
|
|
142
169
|
props: MaybeAccessor<MenuTriggerProps> = {},
|
|
143
|
-
):
|
|
170
|
+
): RootMenuTriggerState {
|
|
144
171
|
const overlay = createOverlayTriggerState(props);
|
|
145
172
|
const [focusStrategy, setFocusStrategy] = createInternalSignal<"first" | "last" | null>(null);
|
|
173
|
+
const [expandedKeysStack, setExpandedKeysStack] = createInternalSignal<Key[]>([]);
|
|
174
|
+
|
|
175
|
+
const openSubmenu = (triggerKey: Key, level: number) => {
|
|
176
|
+
setExpandedKeysStack((oldStack) => {
|
|
177
|
+
if (level > oldStack.length) {
|
|
178
|
+
return oldStack;
|
|
179
|
+
}
|
|
180
|
+
return [...oldStack.slice(0, level), triggerKey];
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const closeSubmenu = (triggerKey: Key, level: number) => {
|
|
185
|
+
setExpandedKeysStack((oldStack) => {
|
|
186
|
+
if (oldStack[level] !== triggerKey) {
|
|
187
|
+
return oldStack;
|
|
188
|
+
}
|
|
189
|
+
return oldStack.slice(0, level);
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const close = () => {
|
|
194
|
+
setExpandedKeysStack([]);
|
|
195
|
+
overlay.close();
|
|
196
|
+
};
|
|
146
197
|
|
|
147
198
|
return {
|
|
148
199
|
...overlay,
|
|
@@ -156,5 +207,84 @@ export function createMenuTriggerState(
|
|
|
156
207
|
setFocusStrategy(strategy);
|
|
157
208
|
overlay.toggle();
|
|
158
209
|
},
|
|
210
|
+
close,
|
|
211
|
+
expandedKeysStack,
|
|
212
|
+
openSubmenu,
|
|
213
|
+
closeSubmenu,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface SubmenuTriggerProps {
|
|
218
|
+
/** Key of the trigger item. */
|
|
219
|
+
triggerKey: Key;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface SubmenuTriggerState {
|
|
223
|
+
/** Whether the submenu is currently open. */
|
|
224
|
+
readonly isOpen: () => boolean;
|
|
225
|
+
/** Controls which item will be auto focused when the submenu opens. */
|
|
226
|
+
readonly focusStrategy: () => "first" | "last" | null;
|
|
227
|
+
/** The level of the submenu, frozen when the state is created. */
|
|
228
|
+
readonly submenuLevel: number;
|
|
229
|
+
/** Opens the submenu. */
|
|
230
|
+
open(focusStrategy?: "first" | "last" | null): void;
|
|
231
|
+
/** Closes the submenu. */
|
|
232
|
+
close(): void;
|
|
233
|
+
/** Closes the root menu and every submenu in the tree. */
|
|
234
|
+
closeAll(): void;
|
|
235
|
+
/** Toggles the submenu. */
|
|
236
|
+
toggle(focusStrategy?: "first" | "last" | null): void;
|
|
237
|
+
/** @private Placeholder so this state can sit where an overlay trigger state sits. */
|
|
238
|
+
setOpen(): void;
|
|
239
|
+
/** Submenus do not track a cursor point. */
|
|
240
|
+
readonly point: null;
|
|
241
|
+
/** @private */
|
|
242
|
+
setPoint(): void;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Creates state for a submenu trigger. Mirrors `@react-stately/menu`
|
|
247
|
+
* `useSubmenuTriggerState`: open state is the root stack entry at this level.
|
|
248
|
+
*/
|
|
249
|
+
export function createSubmenuTriggerState(
|
|
250
|
+
props: MaybeAccessor<SubmenuTriggerProps>,
|
|
251
|
+
state: RootMenuTriggerState,
|
|
252
|
+
): SubmenuTriggerState {
|
|
253
|
+
const getProps = propsAccessor(props);
|
|
254
|
+
const submenuLevel = state.expandedKeysStack().length;
|
|
255
|
+
const [focusStrategy, setFocusStrategy] = createInternalSignal<"first" | "last" | null>(null);
|
|
256
|
+
|
|
257
|
+
const isOpen = () => state.expandedKeysStack()[submenuLevel] === getProps().triggerKey;
|
|
258
|
+
|
|
259
|
+
const open = (strategy?: "first" | "last" | null) => {
|
|
260
|
+
setFocusStrategy(strategy ?? null);
|
|
261
|
+
state.openSubmenu(getProps().triggerKey, submenuLevel);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const close = () => {
|
|
265
|
+
setFocusStrategy(null);
|
|
266
|
+
state.closeSubmenu(getProps().triggerKey, submenuLevel);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const toggle = (strategy?: "first" | "last" | null) => {
|
|
270
|
+
setFocusStrategy(strategy ?? null);
|
|
271
|
+
if (isOpen()) {
|
|
272
|
+
close();
|
|
273
|
+
} else {
|
|
274
|
+
open(strategy);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
focusStrategy,
|
|
280
|
+
isOpen,
|
|
281
|
+
open,
|
|
282
|
+
close,
|
|
283
|
+
closeAll: () => state.close(),
|
|
284
|
+
submenuLevel,
|
|
285
|
+
setOpen() {},
|
|
286
|
+
toggle,
|
|
287
|
+
point: null,
|
|
288
|
+
setPoint() {},
|
|
159
289
|
};
|
|
160
290
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { createMemo } from "solid-js";
|
|
24
24
|
import type { Accessor } from "solid-js";
|
|
25
|
-
import {
|
|
25
|
+
import { createInternalSignal, propsAccessor, readNow, type MaybeAccessor } from "../utils";
|
|
26
26
|
import type {
|
|
27
27
|
Collection,
|
|
28
28
|
DisabledBehavior,
|
|
@@ -117,7 +117,7 @@ export interface SelectionPressEvent {
|
|
|
117
117
|
export function createSelectionState(
|
|
118
118
|
props: MaybeAccessor<SelectionStateProps> = {},
|
|
119
119
|
): SelectionState {
|
|
120
|
-
const getProps = (
|
|
120
|
+
const getProps = propsAccessor(props);
|
|
121
121
|
|
|
122
122
|
// Selection behavior state
|
|
123
123
|
const [internalBehavior, setInternalBehavior] = createInternalSignal<SelectionBehavior>("toggle");
|
package/src/collections/index.ts
CHANGED
|
@@ -46,10 +46,14 @@ export {
|
|
|
46
46
|
export {
|
|
47
47
|
createMenuState,
|
|
48
48
|
createMenuTriggerState,
|
|
49
|
+
createSubmenuTriggerState,
|
|
49
50
|
type MenuStateProps,
|
|
50
51
|
type MenuState,
|
|
51
52
|
type MenuTriggerType,
|
|
52
53
|
type MenuTriggerProps,
|
|
53
54
|
type MenuTriggerStateProps,
|
|
54
55
|
type MenuTriggerState,
|
|
56
|
+
type RootMenuTriggerState,
|
|
57
|
+
type SubmenuTriggerProps,
|
|
58
|
+
type SubmenuTriggerState,
|
|
55
59
|
} from "./createMenuState";
|
|
@@ -45,6 +45,8 @@ export interface ColorAreaStateOptions {
|
|
|
45
45
|
export interface ColorAreaState {
|
|
46
46
|
/** The current color value. */
|
|
47
47
|
readonly value: Color;
|
|
48
|
+
/** The color a native form reset restores. */
|
|
49
|
+
readonly defaultValue: Color;
|
|
48
50
|
/** The X axis channel. */
|
|
49
51
|
readonly xChannel: ColorChannel;
|
|
50
52
|
/** The Y axis channel. */
|
|
@@ -72,6 +74,8 @@ export interface ColorAreaState {
|
|
|
72
74
|
setXValue(value: number): void;
|
|
73
75
|
/** Set the Y channel value. */
|
|
74
76
|
setYValue(value: number): void;
|
|
77
|
+
/** Replace the color. */
|
|
78
|
+
setValue(value: Color | string): void;
|
|
75
79
|
/** Set color from a point (0-1, 0-1). */
|
|
76
80
|
setColorFromPoint(x: number, y: number): void;
|
|
77
81
|
/** Get the thumb position as percentages. */
|
|
@@ -104,6 +108,13 @@ export function createColorAreaState(options: Accessor<ColorAreaStateOptions>):
|
|
|
104
108
|
|
|
105
109
|
// Internal value state
|
|
106
110
|
const [internalValue, setInternalValue] = createInternalSignal<Color | null>(null);
|
|
111
|
+
// Pin useColorAreaState returns the first controlled color as defaultValue
|
|
112
|
+
// so a native form reset restores that color. The built-in red is only the
|
|
113
|
+
// uncontrolled fallback when no defaultValue was passed.
|
|
114
|
+
const firstOptions = getOptions();
|
|
115
|
+
const controlledInitial =
|
|
116
|
+
firstOptions.value !== undefined ? normalizeValue(firstOptions.value) : undefined;
|
|
117
|
+
const initialValue = normalizeValue(firstOptions.defaultValue ?? "#ff0000");
|
|
107
118
|
const [isDragging, setIsDragging] = createInternalSignal(false);
|
|
108
119
|
|
|
109
120
|
// Initialize internal value
|
|
@@ -182,6 +193,10 @@ export function createColorAreaState(options: Accessor<ColorAreaStateOptions>):
|
|
|
182
193
|
// Get Y value
|
|
183
194
|
const getYValue = () => value().getChannelValue(yChannel());
|
|
184
195
|
|
|
196
|
+
const setValue = (newValue: Color | string) => {
|
|
197
|
+
updateValue(normalizeValue(newValue));
|
|
198
|
+
};
|
|
199
|
+
|
|
185
200
|
// Set X value
|
|
186
201
|
const setXValue = (newValue: number) => {
|
|
187
202
|
const range = xRange();
|
|
@@ -283,6 +298,9 @@ export function createColorAreaState(options: Accessor<ColorAreaStateOptions>):
|
|
|
283
298
|
get value() {
|
|
284
299
|
return value();
|
|
285
300
|
},
|
|
301
|
+
get defaultValue() {
|
|
302
|
+
return controlledInitial ?? initialValue;
|
|
303
|
+
},
|
|
286
304
|
get xChannel() {
|
|
287
305
|
return xChannel();
|
|
288
306
|
},
|
|
@@ -314,6 +332,7 @@ export function createColorAreaState(options: Accessor<ColorAreaStateOptions>):
|
|
|
314
332
|
getYValue,
|
|
315
333
|
setXValue,
|
|
316
334
|
setYValue,
|
|
335
|
+
setValue,
|
|
317
336
|
setColorFromPoint,
|
|
318
337
|
getThumbPosition,
|
|
319
338
|
incrementX,
|
|
@@ -57,6 +57,8 @@ export interface ColorFieldState {
|
|
|
57
57
|
readonly value: Color | null;
|
|
58
58
|
/** The color used for channel calculations. */
|
|
59
59
|
readonly colorValue: Color;
|
|
60
|
+
/** The color a native form reset restores. */
|
|
61
|
+
readonly defaultColorValue: Color | null;
|
|
60
62
|
/** The current input text. */
|
|
61
63
|
readonly inputValue: string;
|
|
62
64
|
/** The numeric value exposed by channel fields. */
|
|
@@ -212,6 +214,15 @@ export function createColorFieldState(options: Accessor<ColorFieldStateOptions>)
|
|
|
212
214
|
const getOptions = () => options();
|
|
213
215
|
const initialOptions = getOptions();
|
|
214
216
|
const initialValue = getDefaultValue(initialOptions);
|
|
217
|
+
// Form reset restores the first defaultValue, or the first controlled color
|
|
218
|
+
// when that default was omitted. An explicit null default falls through.
|
|
219
|
+
const initialDefault =
|
|
220
|
+
initialOptions.defaultValue !== undefined
|
|
221
|
+
? normalizeNullableColor(initialOptions.defaultValue)
|
|
222
|
+
: undefined;
|
|
223
|
+
const firstColor =
|
|
224
|
+
initialOptions.value !== undefined ? normalizeNullableColor(initialOptions.value) : undefined;
|
|
225
|
+
const defaultColorValue = initialDefault ?? firstColor ?? null;
|
|
215
226
|
|
|
216
227
|
const [internalValue, setInternalValue] = createInternalSignal<Color | null>(initialValue);
|
|
217
228
|
const [inputValue, setInputValueInternal] = createInternalSignal(
|
|
@@ -224,6 +235,9 @@ export function createColorFieldState(options: Accessor<ColorFieldStateOptions>)
|
|
|
224
235
|
),
|
|
225
236
|
);
|
|
226
237
|
const [invalidInput, setInvalidInput] = createInternalSignal(false);
|
|
238
|
+
// Formatted channel text may round the accepted number. Only user edits
|
|
239
|
+
// should parse that text back into a new numerical value.
|
|
240
|
+
let isChannelInputEdited = false;
|
|
227
241
|
|
|
228
242
|
const channel = createMemo(() => getOptions().channel);
|
|
229
243
|
const colorSpace = createMemo(() => getOptions().colorSpace);
|
|
@@ -249,6 +263,7 @@ export function createColorFieldState(options: Accessor<ColorFieldStateOptions>)
|
|
|
249
263
|
createEffect(
|
|
250
264
|
() => formatCurrentValue(),
|
|
251
265
|
(formatted) => {
|
|
266
|
+
isChannelInputEdited = false;
|
|
252
267
|
setInputValueInternal(formatted);
|
|
253
268
|
setInvalidInput(false);
|
|
254
269
|
},
|
|
@@ -262,11 +277,13 @@ export function createColorFieldState(options: Accessor<ColorFieldStateOptions>)
|
|
|
262
277
|
}
|
|
263
278
|
|
|
264
279
|
opts.onChange?.(newColor);
|
|
280
|
+
isChannelInputEdited = false;
|
|
265
281
|
setInputValueInternal(formatCurrentValue(newColor));
|
|
266
282
|
setInvalidInput(false);
|
|
267
283
|
};
|
|
268
284
|
|
|
269
285
|
const setInputValue = (text: string) => {
|
|
286
|
+
isChannelInputEdited = true;
|
|
270
287
|
setInputValueInternal(text);
|
|
271
288
|
setInvalidInput(false);
|
|
272
289
|
};
|
|
@@ -275,45 +292,74 @@ export function createColorFieldState(options: Accessor<ColorFieldStateOptions>)
|
|
|
275
292
|
updateValue(newColor);
|
|
276
293
|
};
|
|
277
294
|
|
|
295
|
+
// Methods can run before Solid's value memo observes an earlier write.
|
|
296
|
+
const currentValue = () => {
|
|
297
|
+
const opts = getOptions();
|
|
298
|
+
return opts.value !== undefined ? normalizeNullableColor(opts.value) : readNow(internalValue);
|
|
299
|
+
};
|
|
300
|
+
|
|
278
301
|
const commit = () => {
|
|
279
302
|
const text = readNow(inputValue).trim();
|
|
280
303
|
const opts = getOptions();
|
|
281
304
|
const chan = channel();
|
|
305
|
+
const current = currentValue();
|
|
282
306
|
|
|
283
307
|
if (!text) {
|
|
284
|
-
updateValue(null);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (chan) {
|
|
289
|
-
const displayColor = colorValue();
|
|
308
|
+
if (current !== null) updateValue(null);
|
|
309
|
+
} else if (chan) {
|
|
310
|
+
const displayColor = getDisplayColor(current, colorSpace());
|
|
290
311
|
const rawValue = parseChannelValue(text, displayColor, chan);
|
|
291
|
-
if (rawValue
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
312
|
+
if (isChannelInputEdited && rawValue !== null) {
|
|
313
|
+
const next = displayColor.withChannelValue(chan, rawValue);
|
|
314
|
+
if (current === null || next.getChannelValue(chan) !== displayColor.getChannelValue(chan)) {
|
|
315
|
+
updateValue(next);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
try {
|
|
320
|
+
const next = parseHexValue(text, opts.colorFormat);
|
|
321
|
+
// Whole hex edits compare RGB; hexa also edits alpha. This is not
|
|
322
|
+
// the equality policy for channel edits or the public color setter.
|
|
323
|
+
if (
|
|
324
|
+
current === null ||
|
|
325
|
+
next.toHexInt() !== current.toHexInt() ||
|
|
326
|
+
(opts.colorFormat === "hexa" &&
|
|
327
|
+
next.getChannelValue("alpha") !== current.getChannelValue("alpha"))
|
|
328
|
+
) {
|
|
329
|
+
updateValue(next);
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
// Invalid text rolls back to the accepted color below.
|
|
295
333
|
}
|
|
296
|
-
|
|
297
|
-
updateValue(displayColor.withChannelValue(chan, rawValue));
|
|
298
|
-
return;
|
|
299
334
|
}
|
|
300
335
|
|
|
336
|
+
// Read after onChange: a controlled parent may accept or refuse the
|
|
337
|
+
// request. Later signal acceptance is handled by the formatting effect.
|
|
338
|
+
isChannelInputEdited = false;
|
|
339
|
+
setInputValueInternal(formatCurrentValue(currentValue()));
|
|
340
|
+
setInvalidInput(false);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const incrementHex = (amount: number) => {
|
|
344
|
+
// Step the text being edited. A missing or unparseable hex starts at black,
|
|
345
|
+
// matching useColorFieldState's addColorValue(parsedValue, step).
|
|
346
|
+
const text = readNow(inputValue);
|
|
347
|
+
let parsed: Color | null = null;
|
|
301
348
|
try {
|
|
302
|
-
|
|
349
|
+
parsed = parseColor(text.startsWith("#") ? text : `#${text}`);
|
|
303
350
|
} catch {
|
|
304
|
-
|
|
305
|
-
setInvalidInput(false);
|
|
351
|
+
parsed = null;
|
|
306
352
|
}
|
|
307
|
-
};
|
|
308
353
|
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
354
|
+
const base = parsed ?? hexIntToColor(0);
|
|
355
|
+
const nextColor = hexIntToColor(base.toFormat("rgb").toHexInt() + amount);
|
|
356
|
+
const current = value();
|
|
357
|
+
if (current && current.toFormat("rgb").toHexInt() === nextColor.toHexInt()) {
|
|
312
358
|
return;
|
|
313
359
|
}
|
|
314
360
|
|
|
315
|
-
const
|
|
316
|
-
updateValue(
|
|
361
|
+
const format = getOptions().colorFormat;
|
|
362
|
+
updateValue(format ? nextColor.toFormat(format) : nextColor);
|
|
317
363
|
};
|
|
318
364
|
|
|
319
365
|
const updateChannel = (amount: number | "min" | "max") => {
|
|
@@ -322,17 +368,20 @@ export function createColorFieldState(options: Accessor<ColorFieldStateOptions>)
|
|
|
322
368
|
return;
|
|
323
369
|
}
|
|
324
370
|
|
|
325
|
-
const
|
|
371
|
+
const current = currentValue();
|
|
372
|
+
const displayColor = getDisplayColor(current, colorSpace());
|
|
326
373
|
const range = displayColor.getChannelRange(chan);
|
|
327
|
-
const
|
|
374
|
+
const currentChannel = displayColor.getChannelValue(chan);
|
|
328
375
|
const nextValue =
|
|
329
376
|
amount === "min"
|
|
330
377
|
? range.minValue
|
|
331
378
|
: amount === "max"
|
|
332
379
|
? range.maxValue
|
|
333
|
-
: Math.min(range.maxValue, Math.max(range.minValue,
|
|
380
|
+
: Math.min(range.maxValue, Math.max(range.minValue, currentChannel + amount));
|
|
334
381
|
|
|
335
|
-
|
|
382
|
+
const next = displayColor.withChannelValue(chan, nextValue);
|
|
383
|
+
if (current !== null && next.getChannelValue(chan) === currentChannel) return;
|
|
384
|
+
updateValue(next);
|
|
336
385
|
};
|
|
337
386
|
|
|
338
387
|
const increment = () => {
|
|
@@ -397,6 +446,7 @@ export function createColorFieldState(options: Accessor<ColorFieldStateOptions>)
|
|
|
397
446
|
get colorValue() {
|
|
398
447
|
return colorValue();
|
|
399
448
|
},
|
|
449
|
+
defaultColorValue,
|
|
400
450
|
get inputValue() {
|
|
401
451
|
return inputValue();
|
|
402
452
|
},
|