@agnos-ui/core 0.0.1-alpha.3 → 0.0.1-alpha.4

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/accordion.d.ts CHANGED
@@ -126,6 +126,12 @@ export interface AccordionProps extends WidgetsCommonPropsAndState {
126
126
  * It is a prop of the accordion-item.
127
127
  */
128
128
  itemBodyClass: string;
129
+ /**
130
+ * The html tag to use for the accordion-item-header.
131
+ *
132
+ * It is a prop of the accordion-item.
133
+ */
134
+ itemHeadingTag: string;
129
135
  }
130
136
  export interface AccordionState extends WidgetsCommonPropsAndState {
131
137
  /**
@@ -263,6 +269,10 @@ export interface AccordionItemCommonPropsAndState {
263
269
  * Classes to add on the accordion-item body DOM element.
264
270
  */
265
271
  itemBodyClass: string;
272
+ /**
273
+ * The html tag to use for the accordion-item-header.
274
+ */
275
+ itemHeadingTag: string;
266
276
  }
267
277
  export interface AccordionItemProps extends AccordionItemCommonPropsAndState {
268
278
  /**
package/accordion.js CHANGED
@@ -35,11 +35,12 @@ const defaultAccordionConfig = {
35
35
  onHidden: noop,
36
36
  className: '',
37
37
  itemId: '',
38
- itemDestroyOnHide: false,
38
+ itemDestroyOnHide: true,
39
39
  itemDisabled: false,
40
40
  itemVisible: false,
41
41
  itemAnimation: true,
42
42
  itemTransition: collapseVerticalTransition,
43
+ itemHeadingTag: '',
43
44
  onItemShown: noop,
44
45
  onItemHidden: noop,
45
46
  onItemVisibleChange: noop,
@@ -70,6 +71,7 @@ const defaultItemConfig = {
70
71
  itemButtonClass: defaultAccordionConfig.itemButtonClass,
71
72
  itemCollapseClass: defaultAccordionConfig.itemCollapseClass,
72
73
  itemBodyClass: defaultAccordionConfig.itemBodyClass,
74
+ itemHeadingTag: defaultAccordionConfig.itemHeadingTag,
73
75
  };
74
76
  const accordionItemProps = Object.keys(defaultItemConfig);
75
77
  /**
@@ -97,6 +99,7 @@ const configAccordionValidator = {
97
99
  itemButtonClass: typeString,
98
100
  itemCollapseClass: typeString,
99
101
  itemBodyClass: typeString,
102
+ itemHeadingTag: typeString,
100
103
  };
101
104
  const configItemValidator = {
102
105
  itemId: typeString,
@@ -113,6 +116,7 @@ const configItemValidator = {
113
116
  itemButtonClass: typeString,
114
117
  itemCollapseClass: typeString,
115
118
  itemBodyClass: typeString,
119
+ itemHeadingTag: typeString,
116
120
  };
117
121
  function createAccordionItem(accordionOnShown, accordionOnHidden, config) {
118
122
  const [{ itemAnimation$, itemTransition$, itemDestroyOnHide$, onItemShown$, onItemHidden$, onItemVisibleChange$, itemVisible$, itemId$: _dirtyItemId$, itemDisabled$, ...stateProps }, patch,] = writablesForProps(defaultItemConfig, config, configItemValidator);
@@ -178,7 +182,7 @@ function createAccordionItem(accordionOnShown, accordionOnHidden, config) {
178
182
  * @returns a new accordion widget instance
179
183
  */
180
184
  export function createAccordion(config) {
181
- const [{ closeOthers$, onShown$, onHidden$, itemId$, itemAnimation$, itemClass$, itemDisabled$, itemVisible$, itemTransition$, itemDestroyOnHide$, itemBodyClass$, itemButtonClass$, itemCollapseClass$, itemHeaderClass$, onItemVisibleChange$, onItemHidden$, onItemShown$, slotItemStructure$, slotItemBody$, slotItemHeader$, ...stateProps }, patch,] = writablesForProps(defaultAccordionConfig, config, configAccordionValidator);
185
+ const [{ closeOthers$, onShown$, onHidden$, itemId$, itemAnimation$, itemClass$, itemDisabled$, itemVisible$, itemTransition$, itemDestroyOnHide$, itemBodyClass$, itemButtonClass$, itemCollapseClass$, itemHeaderClass$, itemHeadingTag$, onItemVisibleChange$, onItemHidden$, onItemShown$, slotItemStructure$, slotItemBody$, slotItemHeader$, ...stateProps }, patch,] = writablesForProps(defaultAccordionConfig, config, configAccordionValidator);
182
186
  const accordionItemConfig = {
183
187
  itemId: itemId$,
184
188
  itemClass: itemClass$,
@@ -191,6 +195,7 @@ export function createAccordion(config) {
191
195
  itemButtonClass: itemButtonClass$,
192
196
  itemCollapseClass: itemCollapseClass$,
193
197
  itemHeaderClass: itemHeaderClass$,
198
+ itemHeadingTag: itemHeadingTag$,
194
199
  onItemVisibleChange: onItemVisibleChange$,
195
200
  onItemHidden: onItemHidden$,
196
201
  onItemShown: onItemShown$,
package/config.d.ts CHANGED
@@ -6,6 +6,7 @@ import type { RatingProps } from './rating';
6
6
  import type { SelectProps } from './select';
7
7
  import type { AccordionProps } from './accordion';
8
8
  import type { ProgressbarProps } from './progressbar';
9
+ import type { SliderProps } from './slider';
9
10
  export type Partial2Levels<T> = Partial<{
10
11
  [Level1 in keyof T]: Partial<T[Level1]>;
11
12
  }>;
@@ -73,4 +74,8 @@ export interface WidgetsConfig {
73
74
  * the progress bar widget config
74
75
  */
75
76
  progressbar: ProgressbarProps;
77
+ /**
78
+ * slider widget config
79
+ */
80
+ slider: SliderProps;
76
81
  }
package/index.d.ts CHANGED
@@ -12,3 +12,4 @@ export * from './accordion';
12
12
  export * from './commonProps';
13
13
  export * from './progressbar';
14
14
  export * from './extendWidget';
15
+ export * from './slider';
package/index.js CHANGED
@@ -12,3 +12,4 @@ export * from './accordion';
12
12
  export * from './commonProps';
13
13
  export * from './progressbar';
14
14
  export * from './extendWidget';
15
+ export * from './slider';
package/package.json CHANGED
@@ -10,7 +10,8 @@
10
10
  "alert",
11
11
  "modal",
12
12
  "pagination",
13
- "rating"
13
+ "rating",
14
+ "slider"
14
15
  ],
15
16
  "type": "module",
16
17
  "main": "./index.js",
@@ -26,7 +27,7 @@
26
27
  "@amadeus-it-group/tansu": "0.0.23"
27
28
  },
28
29
  "sideEffects": false,
29
- "version": "0.0.1-alpha.3",
30
+ "version": "0.0.1-alpha.4",
30
31
  "homepage": "https://amadeusitgroup.github.io/AgnosUI/latest/",
31
32
  "bugs": "https://github.com/AmadeusITGroup/AgnosUI/issues",
32
33
  "license": "MIT",
@@ -6,3 +6,4 @@ export * from './portal';
6
6
  export * from './stores';
7
7
  export * from './writables';
8
8
  export * from './navManager';
9
+ export * from './isFocusable';
package/services/index.js CHANGED
@@ -6,3 +6,4 @@ export * from './portal';
6
6
  export * from './stores';
7
7
  export * from './writables';
8
8
  export * from './navManager';
9
+ export * from './isFocusable';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns true if the given HTML element is programmatically focusable.
3
+ * Warning: this is a best-effort approximation of whether the element is really focusable.
4
+ * It may not handle all use cases accurately.
5
+ *
6
+ * @param element - element to test
7
+ * @returns true if the element is programmatically focusable.
8
+ */
9
+ export declare const isFocusable: (element: HTMLElement) => boolean;
@@ -0,0 +1,35 @@
1
+ const isInertOrInvisible = (element) => {
2
+ let curElement = element;
3
+ while (curElement) {
4
+ const style = getComputedStyle(curElement);
5
+ if (curElement.inert || curElement.hidden || style.display === 'none' || style.visibility === 'hidden') {
6
+ return true;
7
+ }
8
+ curElement = curElement.parentElement;
9
+ }
10
+ return false;
11
+ };
12
+ const checkNotDisabled = (element) => {
13
+ if (element.disabled) {
14
+ return false;
15
+ }
16
+ const parentFieldset = element.parentElement?.closest('fieldset');
17
+ return parentFieldset ? checkNotDisabled(parentFieldset) : true;
18
+ };
19
+ const isFocusableOtherTags = (element) => element.isContentEditable || !!element.hasAttribute('tabindex');
20
+ const isFocusableByTagName = {
21
+ INPUT: (element) => element.type !== 'hidden' && checkNotDisabled(element),
22
+ SELECT: checkNotDisabled,
23
+ TEXTAREA: checkNotDisabled,
24
+ BUTTON: checkNotDisabled,
25
+ A: (element) => !!element.href || isFocusableOtherTags(element),
26
+ };
27
+ /**
28
+ * Returns true if the given HTML element is programmatically focusable.
29
+ * Warning: this is a best-effort approximation of whether the element is really focusable.
30
+ * It may not handle all use cases accurately.
31
+ *
32
+ * @param element - element to test
33
+ * @returns true if the element is programmatically focusable.
34
+ */
35
+ export const isFocusable = (element) => !isInertOrInvisible(element) && (isFocusableByTagName[element.tagName] ?? isFocusableOtherTags)(element);
package/slider.d.ts ADDED
@@ -0,0 +1,193 @@
1
+ import type { WidgetsCommonPropsAndState } from './commonProps';
2
+ import type { PropsConfig } from './services';
3
+ import type { Directive, Widget } from './types';
4
+ export interface ProgressDisplayOptions {
5
+ /**
6
+ * Left offset of the progress in %
7
+ */
8
+ left: number;
9
+ /**
10
+ * Bottom offset of the progresss in %
11
+ */
12
+ bottom: number;
13
+ /**
14
+ * Width of the progress in %
15
+ */
16
+ width: number;
17
+ /**
18
+ * Height of hte progress in %
19
+ */
20
+ height: number;
21
+ }
22
+ export interface HandleDisplayOptions {
23
+ /**
24
+ * Left offset of the handle in %
25
+ */
26
+ left: number;
27
+ /**
28
+ * Top offset of the handle in %
29
+ */
30
+ top: number;
31
+ }
32
+ export interface SliderCommonPropsAndState extends WidgetsCommonPropsAndState {
33
+ /**
34
+ * Minimum value that can be assigned to the slider
35
+ */
36
+ min: number;
37
+ /**
38
+ * Maximum value that can be assigned to the slider
39
+ */
40
+ max: number;
41
+ /**
42
+ * Unit value between slider steps
43
+ */
44
+ stepSize: number;
45
+ /**
46
+ * If `true` slider value cannot be changed but the slider is still focusable
47
+ */
48
+ readonly: boolean;
49
+ /**
50
+ * If `true` slider value cannot be changed and the slider cannot be focused
51
+ */
52
+ disabled: boolean;
53
+ /**
54
+ * If `true` is vertically positioned otherwise it is horizontal
55
+ */
56
+ vertical: boolean;
57
+ /**
58
+ * Current slider values
59
+ */
60
+ values: number[];
61
+ }
62
+ export interface SliderState extends SliderCommonPropsAndState {
63
+ /**
64
+ * Sorted slider values
65
+ */
66
+ sortedValues: number[];
67
+ /**
68
+ * Combined label left offset in %
69
+ */
70
+ combinedLabelPositionLeft: number;
71
+ /**
72
+ * Combined label top offset in %
73
+ */
74
+ combinedLabelPositionTop: number;
75
+ /**
76
+ * If `visible` the minimum label will be displayed
77
+ */
78
+ minValueLabelDisplay: string;
79
+ /**
80
+ * If `visible` the maximum label will be displayed
81
+ */
82
+ maxValueLabelDisplay: string;
83
+ /**
84
+ * If `visible` the label when the handles are close is displayed
85
+ */
86
+ combinedLabelDisplay: string;
87
+ /**
88
+ * Array of the sorted handles to display
89
+ */
90
+ sortedHandles: {
91
+ value: number;
92
+ id: number;
93
+ }[];
94
+ /**
95
+ * Array of objects representing progress display options
96
+ */
97
+ progressDisplayOptions: ProgressDisplayOptions[];
98
+ /**
99
+ * Array of objects representing handle display options
100
+ */
101
+ handleDisplayOptions: HandleDisplayOptions[];
102
+ }
103
+ export interface SliderProps extends SliderCommonPropsAndState {
104
+ /**
105
+ * An event emitted when slider values are changed
106
+ *
107
+ * Event payload equals to the updated slider values
108
+ */
109
+ onValuesChange: (values: number[]) => void;
110
+ }
111
+ export interface SliderApi {
112
+ }
113
+ export interface SliderDirectives {
114
+ /**
115
+ * Directive to get the slider component elementRef
116
+ */
117
+ sliderDirective: Directive;
118
+ /**
119
+ * Directive to get the minLabel elementRef
120
+ */
121
+ minLabelDirective: Directive;
122
+ /**
123
+ * Directive to get the maxLabel elementRef
124
+ */
125
+ maxLabelDirective: Directive;
126
+ }
127
+ export interface SliderActions {
128
+ /**
129
+ * Method to handle click on the slider
130
+ * @param event - mouse event
131
+ */
132
+ click(event: MouseEvent): void;
133
+ /**
134
+ * Method to process the keyboard event
135
+ * @param event - keyboard event object
136
+ * @param handleNumber - id of the modified handle
137
+ */
138
+ keydown(event: KeyboardEvent, handleNumber: number): void;
139
+ /**
140
+ * Method describing the behavior of the slider handle on mouse down event
141
+ * @param event - mouse event
142
+ * @param handleId - numeric id of the handle
143
+ */
144
+ mouseDown(event: MouseEvent, handleId: number): void;
145
+ }
146
+ export type SliderWidget = Widget<SliderProps, SliderState, SliderApi, SliderActions, SliderDirectives>;
147
+ /**
148
+ * Returns a shallow copy of the default slider config.
149
+ * @returns a copy of the default config
150
+ */
151
+ export declare function getSliderDefaultConfig(): {
152
+ /**
153
+ * An event emitted when slider values are changed
154
+ *
155
+ * Event payload equals to the updated slider values
156
+ */
157
+ onValuesChange: (values: number[]) => void;
158
+ /**
159
+ * Minimum value that can be assigned to the slider
160
+ */
161
+ min: number;
162
+ /**
163
+ * Maximum value that can be assigned to the slider
164
+ */
165
+ max: number;
166
+ /**
167
+ * Unit value between slider steps
168
+ */
169
+ stepSize: number;
170
+ /**
171
+ * If `true` slider value cannot be changed but the slider is still focusable
172
+ */
173
+ readonly: boolean;
174
+ /**
175
+ * If `true` slider value cannot be changed and the slider cannot be focused
176
+ */
177
+ disabled: boolean;
178
+ /**
179
+ * If `true` is vertically positioned otherwise it is horizontal
180
+ */
181
+ vertical: boolean;
182
+ /**
183
+ * Current slider values
184
+ */
185
+ values: number[];
186
+ className: string;
187
+ };
188
+ /**
189
+ * Create a slider widget with given config props
190
+ * @param config - an optional slider config
191
+ * @returns a SliderWidget
192
+ */
193
+ export declare function createSlider(config?: PropsConfig<SliderProps>): SliderWidget;
package/slider.js ADDED
@@ -0,0 +1,289 @@
1
+ import { computed, derived, writable } from '@amadeus-it-group/tansu';
2
+ import { bindableDerived, createStoreDirective, writablesForProps } from './services';
3
+ import { stateStores } from './services/stores';
4
+ import { typeArray, typeBoolean, typeFunction, typeNumber } from './services/writables';
5
+ import { noop } from './utils';
6
+ const defaultSliderConfig = {
7
+ min: 0,
8
+ max: 100,
9
+ stepSize: 1,
10
+ readonly: false,
11
+ disabled: false,
12
+ vertical: false,
13
+ className: '',
14
+ onValuesChange: noop,
15
+ values: [0],
16
+ };
17
+ /**
18
+ * Returns a shallow copy of the default slider config.
19
+ * @returns a copy of the default config
20
+ */
21
+ export function getSliderDefaultConfig() {
22
+ return { ...defaultSliderConfig };
23
+ }
24
+ const configValidator = {
25
+ min: typeNumber,
26
+ max: typeNumber,
27
+ stepSize: typeNumber,
28
+ readonly: typeBoolean,
29
+ disabled: typeBoolean,
30
+ vertical: typeBoolean,
31
+ onValuesChange: typeFunction,
32
+ values: typeArray,
33
+ };
34
+ /**
35
+ * Create a slider widget with given config props
36
+ * @param config - an optional slider config
37
+ * @returns a SliderWidget
38
+ */
39
+ export function createSlider(config) {
40
+ const [{
41
+ // dirty inputs that need adjustment:
42
+ min$: _dirtyMinimum$, max$: _dirtyMaximum$, stepSize$: _dirtyStepSize$, values$: _dirtyValues$, onValuesChange$, ...stateProps }, patch,] = writablesForProps(defaultSliderConfig, config, configValidator);
43
+ const { vertical$, disabled$, readonly$ } = stateProps;
44
+ let _prevCoordinate = -1;
45
+ // clean inputs adjustment
46
+ const min$ = computed(() => {
47
+ if (_dirtyMinimum$() === _dirtyMaximum$())
48
+ return defaultSliderConfig.min;
49
+ return Math.min(_dirtyMinimum$(), _dirtyMaximum$());
50
+ });
51
+ const max$ = computed(() => {
52
+ if (_dirtyMinimum$() === _dirtyMaximum$())
53
+ return defaultSliderConfig.max;
54
+ return Math.max(_dirtyMinimum$(), _dirtyMaximum$());
55
+ });
56
+ const stepSize$ = computed(() => _dirtyStepSize$() || defaultSliderConfig.stepSize);
57
+ const values$ = bindableDerived(onValuesChange$, [_dirtyValues$], ([dirtyValues]) => dirtyValues.map((dv) => computeCleanValue(dv)), (a, b) => a.every((val, index) => val === b[index]));
58
+ // computed
59
+ const { directive: sliderDirective, element$: sliderDom$ } = createStoreDirective();
60
+ const { directive: minLabelDirective, element$: minLabelDom$ } = createStoreDirective();
61
+ const { directive: maxLabelDirective, element$: maxLabelDom$ } = createStoreDirective();
62
+ const sliderResized$ = derived(sliderDom$, (sliderDom, set) => {
63
+ if (!sliderDom) {
64
+ set({});
65
+ return;
66
+ }
67
+ const resizeObserver = new ResizeObserver(() => {
68
+ set({});
69
+ });
70
+ resizeObserver.observe(sliderDom);
71
+ return () => resizeObserver.disconnect();
72
+ }, {});
73
+ const updateSliderSize$ = writable({});
74
+ const sliderDomRect$ = computed(() => {
75
+ sliderResized$();
76
+ updateSliderSize$();
77
+ return sliderDom$()?.getBoundingClientRect() ?? {};
78
+ }, {
79
+ equal: Object.is,
80
+ });
81
+ const minLabelDomRect$ = computed(() => {
82
+ sliderResized$();
83
+ updateSliderSize$();
84
+ return minLabelDom$()?.getBoundingClientRect() ?? {};
85
+ }, {
86
+ equal: (a, b) => Object.is(a, b),
87
+ });
88
+ const maxLabelDomRect$ = computed(() => {
89
+ sliderResized$();
90
+ updateSliderSize$();
91
+ return maxLabelDom$()?.getBoundingClientRect() ?? {};
92
+ }, {
93
+ equal: (a, b) => Object.is(a, b),
94
+ });
95
+ const sliderDomRectOffset$ = computed(() => (vertical$() ? sliderDomRect$().top : sliderDomRect$().left));
96
+ const sliderDomRectSize$ = computed(() => (vertical$() ? sliderDomRect$().height : sliderDomRect$().width));
97
+ const sortedValues$ = computed(() => [...values$()].sort((a, b) => a - b));
98
+ const sortedHandles$ = computed(() => values$()
99
+ .map((val, index) => {
100
+ return { id: index, value: val };
101
+ })
102
+ .sort((a, b) => a.value - b.value));
103
+ const valuesPercent$ = computed(() => values$().map((val) => percentCompute(val)));
104
+ const sortedValuesPercent$ = computed(() => [...valuesPercent$()].sort((a, b) => a - b));
105
+ const minLabelWidth$ = computed(() => (minLabelDomRect$().width / sliderDomRectSize$()) * 100);
106
+ const maxLabelWidth$ = computed(() => (maxLabelDomRect$().width / sliderDomRectSize$()) * 100);
107
+ const minValueLabelDisplay$ = computed(() => (valuesPercent$().some((percent) => percent < minLabelWidth$() + 1) ? 'hidden' : 'visible'));
108
+ const maxValueLabelDisplay$ = computed(() => (valuesPercent$().some((percent) => percent > 100 - maxLabelWidth$() - 1) ? 'hidden' : 'visible'));
109
+ // TODO define the intersection value
110
+ const combinedLabelDisplay$ = computed(() => (values$().length == 2 && Math.abs(values$()[0] - values$()[1]) < 10 ? 'visible' : 'hidden'));
111
+ const isInteractable$ = computed(() => !disabled$() && !readonly$());
112
+ const combinedLabelPositionLeft$ = computed(() => vertical$() || sortedValuesPercent$().length != 2 ? 0 : (sortedValuesPercent$()[0] + sortedValuesPercent$()[1]) / 2);
113
+ const combinedLabelPositionTop$ = computed(() => vertical$() && sortedValuesPercent$().length == 2 ? 100 - (sortedValuesPercent$()[0] + sortedValuesPercent$()[1]) / 2 : 0);
114
+ // const handleTooltipLeft$ = computed(() => (vertical$() ? Array(valuesPercent$().length).fill(0) : valuesPercent$()));
115
+ // const handleTooltipTop$ = computed(() => (vertical$() ? valuesPercent$().map((vp) => 100 - vp) : Array(valuesPercent$().length).fill(0)));
116
+ const handleDisplayOptions$ = computed(() => valuesPercent$().map((vp) => {
117
+ return {
118
+ left: vertical$() ? 0 : vp,
119
+ top: vertical$() ? 100 - vp : 0,
120
+ };
121
+ }));
122
+ const progressDisplayOptions$ = computed(() => {
123
+ if (sortedValuesPercent$().length === 1) {
124
+ return [
125
+ {
126
+ left: 0,
127
+ bottom: 0,
128
+ width: vertical$() ? 100 : sortedValuesPercent$()[0],
129
+ height: vertical$() ? sortedValuesPercent$()[0] : 100,
130
+ },
131
+ ];
132
+ }
133
+ else {
134
+ return sortedValuesPercent$()
135
+ .map((svp, index, array) => {
136
+ return {
137
+ left: vertical$() ? 0 : svp,
138
+ bottom: vertical$() ? svp : 0,
139
+ width: vertical$() ? 100 : index === array.length - 1 ? svp : array[index + 1] - svp,
140
+ height: vertical$() ? (index === array.length - 1 ? svp : array[index + 1] - svp) : 100,
141
+ };
142
+ })
143
+ .slice(0, sortedValuesPercent$().length - 1);
144
+ }
145
+ });
146
+ // functions
147
+ const computeCleanValue = (value) => {
148
+ if (value >= max$()) {
149
+ return max$();
150
+ }
151
+ else if (value <= min$()) {
152
+ return min$();
153
+ }
154
+ const indexMin = Math.floor(value / stepSize$());
155
+ return value % stepSize$() < stepSize$() / 2 ? indexMin * stepSize$() : (indexMin + 1) * stepSize$();
156
+ };
157
+ const percentCompute = (value) => {
158
+ return ((value - min$()) * 100) / (max$() - min$());
159
+ };
160
+ const getClosestSliderHandle = (clickedPercent) => {
161
+ if (values$().length === 1) {
162
+ return 0;
163
+ }
164
+ const closestBigger = sortedValues$().find((sv) => sv > clickedPercent * 100);
165
+ const closestBiggerIndex = closestBigger ? sortedValues$().indexOf(closestBigger) : sortedValues$().length - 1;
166
+ const midPoint = sortedValues$()[closestBiggerIndex - 1] + (sortedValues$()[closestBiggerIndex] - sortedValues$()[closestBiggerIndex - 1]) / 2;
167
+ let closestValue;
168
+ if (clickedPercent * 100 <= midPoint) {
169
+ closestValue = sortedValues$()[closestBiggerIndex - 1];
170
+ }
171
+ else {
172
+ closestValue = sortedValues$()[closestBiggerIndex];
173
+ }
174
+ return values$().indexOf(closestValue);
175
+ };
176
+ const adjustCoordinate = (clickedCoordinate, handleNumber) => {
177
+ if (isInteractable$()) {
178
+ const clickedPercent = vertical$()
179
+ ? (sliderDomRectSize$() - clickedCoordinate + sliderDomRectOffset$()) / sliderDomRectSize$()
180
+ : (clickedCoordinate - sliderDomRectOffset$()) / sliderDomRectSize$();
181
+ const derivedHandleIndex = handleNumber ?? getClosestSliderHandle(clickedPercent);
182
+ const newValue = clickedPercent * (max$() - min$()) + min$();
183
+ _dirtyValues$.update((dh) => {
184
+ dh = [...dh];
185
+ dh[derivedHandleIndex] = newValue;
186
+ return dh;
187
+ });
188
+ }
189
+ };
190
+ return {
191
+ ...stateStores({
192
+ min$,
193
+ max$,
194
+ stepSize$,
195
+ values$,
196
+ sortedValues$,
197
+ sortedHandles$,
198
+ minValueLabelDisplay$,
199
+ maxValueLabelDisplay$,
200
+ combinedLabelDisplay$,
201
+ isInteractable$,
202
+ combinedLabelPositionLeft$,
203
+ combinedLabelPositionTop$,
204
+ progressDisplayOptions$,
205
+ handleDisplayOptions$,
206
+ ...stateProps,
207
+ }),
208
+ patch,
209
+ api: {},
210
+ directives: {
211
+ sliderDirective,
212
+ minLabelDirective,
213
+ maxLabelDirective,
214
+ },
215
+ actions: {
216
+ click(event) {
217
+ updateSliderSize$.set({});
218
+ adjustCoordinate(vertical$() ? event.clientY : event.clientX);
219
+ },
220
+ keydown(event, handleIndex) {
221
+ const { key } = event;
222
+ if (isInteractable$()) {
223
+ switch (key) {
224
+ case 'ArrowDown':
225
+ case 'ArrowLeft':
226
+ _dirtyValues$.update((value) => {
227
+ value = [...value];
228
+ value[handleIndex] = values$()[handleIndex] - stepSize$();
229
+ return value;
230
+ });
231
+ break;
232
+ case 'ArrowUp':
233
+ case 'ArrowRight':
234
+ _dirtyValues$.update((value) => {
235
+ value = [...value];
236
+ value[handleIndex] = values$()[handleIndex] + stepSize$();
237
+ return value;
238
+ });
239
+ break;
240
+ case 'Home':
241
+ _dirtyValues$.update((value) => {
242
+ value = [...value];
243
+ value[handleIndex] = min$();
244
+ return value;
245
+ });
246
+ break;
247
+ case 'End':
248
+ _dirtyValues$.update((value) => {
249
+ value = [...value];
250
+ value[handleIndex] = max$();
251
+ return value;
252
+ });
253
+ break;
254
+ case 'PageUp':
255
+ // TODO it is optional in accessibility guidelines, so define the skip value for steps and write unit test
256
+ break;
257
+ case 'PageDown':
258
+ // TODO it is optional in accessibility guidelines, so define the skip value for steps and write unit test
259
+ break;
260
+ default:
261
+ return;
262
+ }
263
+ event.preventDefault();
264
+ event.stopPropagation();
265
+ }
266
+ },
267
+ mouseDown(event, handleId) {
268
+ event.preventDefault();
269
+ const handleDrag = (e) => {
270
+ e.preventDefault();
271
+ const newCoord = vertical$() ? e.clientY : e.clientX;
272
+ event.target.focus();
273
+ if (_prevCoordinate !== newCoord) {
274
+ _prevCoordinate = newCoord;
275
+ adjustCoordinate(newCoord, handleId);
276
+ }
277
+ };
278
+ if (isInteractable$()) {
279
+ event.target.focus();
280
+ document.addEventListener('mousemove', handleDrag);
281
+ // TODO mouse up doesn't work outside the handle area
282
+ document.addEventListener('mouseup', () => {
283
+ document.removeEventListener('mousemove', handleDrag);
284
+ }, { once: true });
285
+ }
286
+ },
287
+ },
288
+ };
289
+ }