pxt-core 7.5.8 → 7.5.11
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/built/pxt.js +1004 -4
- package/built/pxtblockly.js +439 -49
- package/built/pxtblocks.d.ts +34 -0
- package/built/pxtblocks.js +439 -49
- package/built/pxtlib.d.ts +20 -2
- package/built/pxtlib.js +127 -3
- package/built/pxtsim.d.ts +222 -0
- package/built/pxtsim.js +877 -1
- package/built/target.js +1 -1
- package/built/web/icons.css +49 -10
- package/built/web/main.js +1 -1
- package/built/web/pxtapp.js +1 -1
- package/built/web/pxtasseteditor.js +1 -1
- package/built/web/pxtblockly.js +1 -1
- package/built/web/pxtblocks.js +1 -1
- package/built/web/pxtembed.js +2 -2
- package/built/web/pxtlib.js +1 -1
- package/built/web/pxtsim.js +1 -1
- package/built/web/pxtworker.js +1 -1
- package/built/web/react-common-authcode.css +130 -1
- package/built/web/react-common-skillmap.css +1 -1
- package/built/web/rtlreact-common-skillmap.css +1 -1
- package/built/web/rtlsemantic.css +2 -2
- package/built/web/semantic.css +2 -2
- package/built/web/skillmap/js/main.e30f6be4.chunk.js +1 -0
- package/docfiles/footer.html +1 -1
- package/docfiles/script.html +1 -1
- package/docfiles/thin-footer.html +1 -1
- package/localtypings/pxtarget.d.ts +1 -0
- package/package.json +1 -1
- package/react-common/components/controls/Button.tsx +10 -4
- package/react-common/components/controls/DraggableGraph.tsx +242 -0
- package/react-common/components/controls/Dropdown.tsx +121 -0
- package/react-common/components/controls/FocusList.tsx +17 -8
- package/react-common/components/controls/Input.tsx +13 -3
- package/react-common/components/controls/RadioButtonGroup.tsx +66 -0
- package/react-common/components/util.tsx +23 -0
- package/react-common/styles/controls/Button.less +21 -0
- package/react-common/styles/controls/DraggableGraph.less +13 -0
- package/react-common/styles/controls/Dropdown.less +68 -0
- package/react-common/styles/controls/RadioButtonGroup.less +36 -0
- package/react-common/styles/react-common-variables.less +38 -0
- package/react-common/styles/react-common.less +3 -0
- package/theme/pxt.less +1 -0
- package/theme/soundeffecteditor.less +239 -0
- package/webapp/public/skillmap.html +1 -1
- package/built/web/skillmap/js/main.2485091f.chunk.js +0 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ControlProps, screenToSVGCoord, clientCoord, classList } from "../util";
|
|
3
|
+
|
|
4
|
+
export interface DraggableGraphProps extends ControlProps {
|
|
5
|
+
interpolation: pxt.assets.SoundInterpolation;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
squiggly?: boolean;
|
|
9
|
+
|
|
10
|
+
aspectRatio: number; // width / height
|
|
11
|
+
onPointChange: (index: number, newValue: number) => void;
|
|
12
|
+
|
|
13
|
+
// Points are equally spaced y-values along the width of the graph
|
|
14
|
+
points: number[];
|
|
15
|
+
handleStartAnimationRef?: (startAnimation: (duration: number) => void) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const DraggableGraph = (props: DraggableGraphProps) => {
|
|
19
|
+
const {
|
|
20
|
+
interpolation,
|
|
21
|
+
min,
|
|
22
|
+
max,
|
|
23
|
+
points,
|
|
24
|
+
handleStartAnimationRef,
|
|
25
|
+
onPointChange,
|
|
26
|
+
id,
|
|
27
|
+
className,
|
|
28
|
+
ariaLabel,
|
|
29
|
+
ariaHidden,
|
|
30
|
+
ariaDescribedBy,
|
|
31
|
+
role,
|
|
32
|
+
aspectRatio,
|
|
33
|
+
squiggly
|
|
34
|
+
} = props;
|
|
35
|
+
|
|
36
|
+
const width = 1000;
|
|
37
|
+
const height = (1 / aspectRatio) * width;
|
|
38
|
+
|
|
39
|
+
const unit = width / 40;
|
|
40
|
+
const halfUnit = unit / 2;
|
|
41
|
+
|
|
42
|
+
const yOffset = unit;
|
|
43
|
+
const availableHeight = height - unit * 5 / 2;
|
|
44
|
+
const availableWidth = width - halfUnit * 3;
|
|
45
|
+
|
|
46
|
+
const xSlice = availableWidth / (points.length - 1);
|
|
47
|
+
const yScale = availableHeight / (max - min);
|
|
48
|
+
|
|
49
|
+
const [dragIndex, setDragIndex] = React.useState(-1);
|
|
50
|
+
|
|
51
|
+
const svgCoordToValue = (point: DOMPoint) =>
|
|
52
|
+
(1 - ((point.y - yOffset) / availableHeight)) * (max - min) + min;
|
|
53
|
+
|
|
54
|
+
let animationRef: number;
|
|
55
|
+
|
|
56
|
+
const throttledSetDragValue = (index: number, value: number) => {
|
|
57
|
+
if (animationRef) cancelAnimationFrame(animationRef);
|
|
58
|
+
animationRef = requestAnimationFrame(() => {
|
|
59
|
+
handlePointChange(index, value);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handlePointChange = (index: number, newValue: number) => {
|
|
64
|
+
onPointChange(index, Math.max(Math.min(newValue, max), min));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const refs: SVGRectElement[] = [];
|
|
68
|
+
|
|
69
|
+
const getPointRefHandler = (index: number) =>
|
|
70
|
+
(ref: SVGRectElement) => {
|
|
71
|
+
if (!ref) return;
|
|
72
|
+
|
|
73
|
+
refs[index] = ref;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
refs.forEach((ref, index) => {
|
|
78
|
+
ref.onpointerdown = ev => {
|
|
79
|
+
if (dragIndex !== -1) return;
|
|
80
|
+
const coord = clientCoord(ev);
|
|
81
|
+
const svg = screenToSVGCoord(ref.ownerSVGElement, coord);
|
|
82
|
+
setDragIndex(index);
|
|
83
|
+
throttledSetDragValue(index, svgCoordToValue(svg));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
ref.onpointermove = ev => {
|
|
87
|
+
if (dragIndex !== index) return;
|
|
88
|
+
const coord = clientCoord(ev);
|
|
89
|
+
const svg = screenToSVGCoord(ref.ownerSVGElement, coord);
|
|
90
|
+
throttledSetDragValue(index, svgCoordToValue(svg));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
ref.onpointerleave = ev => {
|
|
94
|
+
if (dragIndex !== index) return;
|
|
95
|
+
setDragIndex(-1);
|
|
96
|
+
const coord = clientCoord(ev);
|
|
97
|
+
const svg = screenToSVGCoord(ref.ownerSVGElement, coord);
|
|
98
|
+
throttledSetDragValue(index, svgCoordToValue(svg));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
ref.onpointerup = ev => {
|
|
102
|
+
if (dragIndex !== index) return;
|
|
103
|
+
setDragIndex(-1);
|
|
104
|
+
const coord = clientCoord(ev);
|
|
105
|
+
const svg = screenToSVGCoord(ref.ownerSVGElement, coord);
|
|
106
|
+
throttledSetDragValue(index, svgCoordToValue(svg));
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}, [dragIndex, onPointChange])
|
|
110
|
+
|
|
111
|
+
const getValue = (index: number) => {
|
|
112
|
+
return points[index];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const handleRectAnimateRef = (ref: SVGAnimateElement) => {
|
|
116
|
+
if (ref && handleStartAnimationRef) {
|
|
117
|
+
handleStartAnimationRef((duration: number) => {
|
|
118
|
+
if (duration <= 0) duration = 1;
|
|
119
|
+
ref.setAttribute("dur", duration + "ms");
|
|
120
|
+
(ref as any).beginElement();
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return <div
|
|
126
|
+
id={id}
|
|
127
|
+
className={classList("common-draggable-graph", className)}
|
|
128
|
+
aria-label={ariaLabel}
|
|
129
|
+
aria-hidden={ariaHidden}
|
|
130
|
+
aria-describedby={ariaDescribedBy}
|
|
131
|
+
role={role}>
|
|
132
|
+
<svg viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg">
|
|
133
|
+
{points.map((val, index) => {
|
|
134
|
+
const isNotLast = index < points.length - 1;
|
|
135
|
+
const x = Math.max(xSlice * index - halfUnit, unit);
|
|
136
|
+
const y = yOffset + Math.max(yScale * (max - getValue(index)) - halfUnit, halfUnit);
|
|
137
|
+
|
|
138
|
+
// The logarithmic interpolation is perpendicular to the x-axis at the beginning, so
|
|
139
|
+
// flip the label to the other side if it would overlap path
|
|
140
|
+
const shouldFlipLabel = isNotLast && interpolation === "logarithmic" && getValue(index + 1) > getValue(index);
|
|
141
|
+
|
|
142
|
+
return <g key={index}>
|
|
143
|
+
<rect
|
|
144
|
+
className="draggable-graph-point"
|
|
145
|
+
x={x}
|
|
146
|
+
y={y}
|
|
147
|
+
width={unit}
|
|
148
|
+
height={unit}
|
|
149
|
+
/>
|
|
150
|
+
{isNotLast &&
|
|
151
|
+
<path
|
|
152
|
+
className="draggable-graph-path"
|
|
153
|
+
stroke="black"
|
|
154
|
+
fill="none"
|
|
155
|
+
strokeWidth="2px"
|
|
156
|
+
d={getInterpolationPath(
|
|
157
|
+
x + halfUnit,
|
|
158
|
+
y + halfUnit,
|
|
159
|
+
Math.max(xSlice * (index + 1), 0),
|
|
160
|
+
yOffset + Math.max(yScale * (max - getValue(index + 1)) - halfUnit, halfUnit) + halfUnit,
|
|
161
|
+
interpolation,
|
|
162
|
+
squiggly
|
|
163
|
+
)}
|
|
164
|
+
/>
|
|
165
|
+
}
|
|
166
|
+
<text x={x + halfUnit} y={shouldFlipLabel ? y + unit * 2 : y - halfUnit} fontSize={unit} className="common-draggable-graph-text">
|
|
167
|
+
{Math.round(getValue(index))}
|
|
168
|
+
</text>
|
|
169
|
+
<rect
|
|
170
|
+
className="draggable-graph-surface"
|
|
171
|
+
ref={getPointRefHandler(index)}
|
|
172
|
+
x={x - xSlice / 6}
|
|
173
|
+
y={0}
|
|
174
|
+
width={xSlice / 3}
|
|
175
|
+
height={height}
|
|
176
|
+
fill="white"
|
|
177
|
+
opacity={0}
|
|
178
|
+
/>
|
|
179
|
+
</g>
|
|
180
|
+
})}
|
|
181
|
+
<rect x="-2" y="0" width="1" height="100%" fill="grey">
|
|
182
|
+
<animate ref={handleRectAnimateRef} attributeName="x" from="2%" to="98%" dur="1000ms" begin="indefinite" />
|
|
183
|
+
</rect>
|
|
184
|
+
</svg>
|
|
185
|
+
</div>
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getInterpolationPath(x0: number, y0: number, x1: number, y1: number, curve: pxt.assets.SoundInterpolation, squiggly: boolean) {
|
|
189
|
+
let pathFunction: (x: number) => number;
|
|
190
|
+
|
|
191
|
+
switch (curve) {
|
|
192
|
+
case "linear":
|
|
193
|
+
pathFunction = x => y0 + (x - x0) * (y1 - y0) / (x1 - x0);
|
|
194
|
+
break;
|
|
195
|
+
case "curve":
|
|
196
|
+
pathFunction = x => y0 + (y1 - y0) * Math.sin((x - x0) / (x1 - x0) * (Math.PI / 2));
|
|
197
|
+
break;
|
|
198
|
+
case "logarithmic":
|
|
199
|
+
pathFunction = x => y0 + Math.log10(1 + 9 * ((x - x0) / (x1 - x0))) * (y1 - y0)
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const slices = 20;
|
|
204
|
+
const slice = (x1 - x0) / slices;
|
|
205
|
+
|
|
206
|
+
const parts: string[] = [`M ${x0} ${y0}`];
|
|
207
|
+
|
|
208
|
+
let prevX = x0;
|
|
209
|
+
let prevY = y0;
|
|
210
|
+
|
|
211
|
+
let currX = x0;
|
|
212
|
+
let currY = y0;
|
|
213
|
+
|
|
214
|
+
const squiggleAmplitude = 40;
|
|
215
|
+
|
|
216
|
+
for (let i = 1; i < slices + 1; i++) {
|
|
217
|
+
currX = x0 + i * slice;
|
|
218
|
+
currY = pathFunction(currX);
|
|
219
|
+
if (!squiggly) {
|
|
220
|
+
parts.push(`L ${currX} ${currY}`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const angle = Math.atan2(currY - prevY, currX - prevX);
|
|
225
|
+
const distance = Math.sqrt((currY - prevY) ** 2 + (currX - prevX) ** 2);
|
|
226
|
+
|
|
227
|
+
const cx1 = prevX + Math.cos(angle) * (distance / 4) + squiggleAmplitude * Math.cos(angle + Math.PI / 2);
|
|
228
|
+
const cy1 = prevY + Math.sin(angle) * (distance / 4) + squiggleAmplitude * Math.sin(angle + Math.PI / 2);
|
|
229
|
+
|
|
230
|
+
parts.push(`Q ${cx1} ${cy1} ${prevX + Math.cos(angle) * (distance / 2)} ${prevY + Math.sin(angle) * (distance / 2)}`)
|
|
231
|
+
|
|
232
|
+
const cx2 = prevX + Math.cos(angle) * (3 * distance / 4) - squiggleAmplitude * Math.cos(angle + Math.PI / 2);
|
|
233
|
+
const cy2 = prevY + Math.sin(angle) * (3 * distance / 4) - squiggleAmplitude * Math.sin(angle + Math.PI / 2);
|
|
234
|
+
|
|
235
|
+
parts.push(`Q ${cx2} ${cy2} ${currX} ${currY}`)
|
|
236
|
+
|
|
237
|
+
prevX = currX;
|
|
238
|
+
prevY = currY;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return parts.join(" ");
|
|
242
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { classList, ControlProps } from "../util";
|
|
3
|
+
import { Button, ButtonViewProps } from "./Button";
|
|
4
|
+
import { FocusList } from "./FocusList";
|
|
5
|
+
|
|
6
|
+
export interface DropdownItem extends ButtonViewProps {
|
|
7
|
+
id: string;
|
|
8
|
+
role?: "option" | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DropdownProps extends ControlProps {
|
|
12
|
+
id: string;
|
|
13
|
+
selectedId: string;
|
|
14
|
+
items: DropdownItem[];
|
|
15
|
+
onItemSelected: (id: string) => void;
|
|
16
|
+
tabIndex?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Dropdown = (props: DropdownProps) => {
|
|
20
|
+
const {
|
|
21
|
+
id,
|
|
22
|
+
className,
|
|
23
|
+
ariaHidden,
|
|
24
|
+
ariaLabel,
|
|
25
|
+
role,
|
|
26
|
+
items,
|
|
27
|
+
tabIndex,
|
|
28
|
+
selectedId,
|
|
29
|
+
onItemSelected
|
|
30
|
+
} = props;
|
|
31
|
+
|
|
32
|
+
const [ expanded, setExpanded ] = React.useState(false);
|
|
33
|
+
|
|
34
|
+
let container: HTMLDivElement;
|
|
35
|
+
|
|
36
|
+
const handleContainerRef = (ref: HTMLDivElement) => {
|
|
37
|
+
if (!ref) return;
|
|
38
|
+
container = ref;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const onMenuButtonClick = () => {
|
|
42
|
+
setExpanded(!expanded);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onBlur = (e: React.FocusEvent) => {
|
|
46
|
+
if (!container) return;
|
|
47
|
+
if (expanded && !container.contains(e.relatedTarget as HTMLElement)) setExpanded(false);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const classes = classList("common-dropdown", className);
|
|
51
|
+
|
|
52
|
+
const selected = items.find(item => item.id === selectedId) || items[0];
|
|
53
|
+
const onItemFocused = (element: HTMLElement) => {
|
|
54
|
+
if (element.id && items.some(item => item.id === element.id)) onItemSelected(element.id);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const onKeyDown = (e: React.KeyboardEvent) => {
|
|
58
|
+
const selectedIndex = items.indexOf(selected)
|
|
59
|
+
|
|
60
|
+
if (e.key === "ArrowDown") {
|
|
61
|
+
if (selectedIndex < items.length - 1) {
|
|
62
|
+
onItemSelected(items[selectedIndex + 1].id);
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (e.key === "ArrowUp") {
|
|
68
|
+
if (selectedIndex > 0) {
|
|
69
|
+
onItemSelected(items[selectedIndex - 1].id);
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (e.key === "Enter") {
|
|
75
|
+
setExpanded(true);
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return <div className={classes} ref={handleContainerRef} onBlur={onBlur}>
|
|
82
|
+
<Button
|
|
83
|
+
{...selected}
|
|
84
|
+
id={id}
|
|
85
|
+
tabIndex={tabIndex}
|
|
86
|
+
rightIcon={expanded ? "fas fa-chevron-up" : "fas fa-chevron-down"}
|
|
87
|
+
role={role}
|
|
88
|
+
className={classList("common-dropdown-button", expanded && "expanded", selected.className)}
|
|
89
|
+
onClick={onMenuButtonClick}
|
|
90
|
+
onKeydown={onKeyDown}
|
|
91
|
+
ariaHasPopup="listbox"
|
|
92
|
+
ariaExpanded={expanded}
|
|
93
|
+
ariaLabel={ariaLabel}
|
|
94
|
+
ariaHidden={ariaHidden}
|
|
95
|
+
/>
|
|
96
|
+
{expanded &&
|
|
97
|
+
<FocusList role="listbox"
|
|
98
|
+
className="common-menu-dropdown-pane common-dropdown-shadow"
|
|
99
|
+
childTabStopId={selectedId}
|
|
100
|
+
aria-labelledby={id}
|
|
101
|
+
useUpAndDownArrowKeys={true}
|
|
102
|
+
onItemReceivedFocus={onItemFocused}>
|
|
103
|
+
<ul role="presentation">
|
|
104
|
+
{ items.map(item =>
|
|
105
|
+
<li key={item.id} role="presentation">
|
|
106
|
+
<Button
|
|
107
|
+
{...item}
|
|
108
|
+
className={classList("common-dropdown-item", item.className)}
|
|
109
|
+
onClick={() => {
|
|
110
|
+
setExpanded(false);
|
|
111
|
+
onItemSelected(item.id);
|
|
112
|
+
}}
|
|
113
|
+
ariaSelected={item.id === selectedId}
|
|
114
|
+
role="option"/>
|
|
115
|
+
</li>
|
|
116
|
+
)}
|
|
117
|
+
</ul>
|
|
118
|
+
</FocusList>
|
|
119
|
+
}
|
|
120
|
+
</div>
|
|
121
|
+
}
|
|
@@ -4,6 +4,8 @@ import { ContainerProps } from "../util";
|
|
|
4
4
|
export interface FocusListProps extends ContainerProps {
|
|
5
5
|
role: string;
|
|
6
6
|
childTabStopId?: string;
|
|
7
|
+
useUpAndDownArrowKeys?: boolean;
|
|
8
|
+
onItemReceivedFocus?: (item: HTMLElement) => void;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -23,6 +25,8 @@ export const FocusList = (props: FocusListProps) => {
|
|
|
23
25
|
ariaLabel,
|
|
24
26
|
childTabStopId,
|
|
25
27
|
children,
|
|
28
|
+
onItemReceivedFocus,
|
|
29
|
+
useUpAndDownArrowKeys
|
|
26
30
|
} = props;
|
|
27
31
|
|
|
28
32
|
let focusableElements: HTMLElement[];
|
|
@@ -59,6 +63,11 @@ export const FocusList = (props: FocusListProps) => {
|
|
|
59
63
|
const target = document.activeElement as HTMLElement;
|
|
60
64
|
const index = focusableElements.indexOf(target);
|
|
61
65
|
|
|
66
|
+
const focus = (element: HTMLElement) => {
|
|
67
|
+
element.focus();
|
|
68
|
+
if (onItemReceivedFocus) onItemReceivedFocus(element);
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
if (index === -1 && target !== focusList) return;
|
|
63
72
|
|
|
64
73
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -73,33 +82,33 @@ export const FocusList = (props: FocusListProps) => {
|
|
|
73
82
|
target.dispatchEvent(new Event("click"));
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
|
-
else if (e.key === "ArrowRight") {
|
|
85
|
+
else if (e.key === (useUpAndDownArrowKeys ? "ArrowDown" : "ArrowRight")) {
|
|
77
86
|
if (index === focusableElements.length - 1 || target === focusList) {
|
|
78
|
-
focusableElements[0]
|
|
87
|
+
focus(focusableElements[0]);
|
|
79
88
|
}
|
|
80
89
|
else {
|
|
81
|
-
focusableElements[index + 1]
|
|
90
|
+
focus(focusableElements[index + 1]);
|
|
82
91
|
}
|
|
83
92
|
e.preventDefault();
|
|
84
93
|
e.stopPropagation();
|
|
85
94
|
}
|
|
86
|
-
else if (e.key === "ArrowLeft") {
|
|
95
|
+
else if (e.key === (useUpAndDownArrowKeys ? "ArrowUp" : "ArrowLeft")) {
|
|
87
96
|
if (index === 0 || target === focusList) {
|
|
88
|
-
focusableElements[focusableElements.length - 1]
|
|
97
|
+
focus(focusableElements[focusableElements.length - 1]);
|
|
89
98
|
}
|
|
90
99
|
else {
|
|
91
|
-
focusableElements[Math.max(index - 1, 0)]
|
|
100
|
+
focus(focusableElements[Math.max(index - 1, 0)]);
|
|
92
101
|
}
|
|
93
102
|
e.preventDefault();
|
|
94
103
|
e.stopPropagation();
|
|
95
104
|
}
|
|
96
105
|
else if (e.key === "Home") {
|
|
97
|
-
focusableElements[0]
|
|
106
|
+
focus(focusableElements[0]);
|
|
98
107
|
e.preventDefault();
|
|
99
108
|
e.stopPropagation();
|
|
100
109
|
}
|
|
101
110
|
else if (e.key === "End") {
|
|
102
|
-
focusableElements[focusableElements.length - 1]
|
|
111
|
+
focus(focusableElements[focusableElements.length - 1]);
|
|
103
112
|
e.preventDefault();
|
|
104
113
|
e.stopPropagation();
|
|
105
114
|
}
|
|
@@ -19,6 +19,7 @@ export interface InputProps extends ControlProps {
|
|
|
19
19
|
onChange?: (newValue: string) => void;
|
|
20
20
|
onEnterKey?: (value: string) => void;
|
|
21
21
|
onIconClick?: (value: string) => void;
|
|
22
|
+
onBlur?: (value: string) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export const Input = (props: InputProps) => {
|
|
@@ -41,10 +42,11 @@ export const Input = (props: InputProps) => {
|
|
|
41
42
|
selectOnClick,
|
|
42
43
|
onChange,
|
|
43
44
|
onEnterKey,
|
|
44
|
-
onIconClick
|
|
45
|
+
onIconClick,
|
|
46
|
+
onBlur
|
|
45
47
|
} = props;
|
|
46
48
|
|
|
47
|
-
const [value, setValue] = React.useState(
|
|
49
|
+
const [value, setValue] = React.useState(undefined);
|
|
48
50
|
|
|
49
51
|
const clickHandler = (evt: React.MouseEvent<any>) => {
|
|
50
52
|
if (selectOnClick) {
|
|
@@ -76,6 +78,13 @@ export const Input = (props: InputProps) => {
|
|
|
76
78
|
if (onIconClick) onIconClick(value);
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
const blurHandler = () => {
|
|
82
|
+
if (onBlur) {
|
|
83
|
+
onBlur(value);
|
|
84
|
+
}
|
|
85
|
+
setValue(undefined);
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
return (
|
|
80
89
|
<div className={classList("common-input-wrapper", disabled && "disabled", className)}>
|
|
81
90
|
{label && <label className="common-input-label" htmlFor={id}>
|
|
@@ -92,11 +101,12 @@ export const Input = (props: InputProps) => {
|
|
|
92
101
|
aria-hidden={ariaHidden}
|
|
93
102
|
type={type || "text"}
|
|
94
103
|
placeholder={placeholder}
|
|
95
|
-
value={value ||
|
|
104
|
+
value={value || initialValue || ""}
|
|
96
105
|
readOnly={!!readOnly}
|
|
97
106
|
onClick={clickHandler}
|
|
98
107
|
onChange={changeHandler}
|
|
99
108
|
onKeyDown={enterKeyHandler}
|
|
109
|
+
onBlur={blurHandler}
|
|
100
110
|
autoComplete={autoComplete ? "" : "off"}
|
|
101
111
|
autoCorrect={autoComplete ? "" : "off"}
|
|
102
112
|
autoCapitalize={autoComplete ? "" : "off"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { classList, ControlProps } from "../util";
|
|
3
|
+
import { FocusList } from "./FocusList";
|
|
4
|
+
|
|
5
|
+
export interface RadioButtonGroupProps extends ControlProps {
|
|
6
|
+
id: string;
|
|
7
|
+
choices: RadioGroupChoice[];
|
|
8
|
+
selectedId: string;
|
|
9
|
+
onChoiceSelected: (id: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RadioGroupChoice {
|
|
13
|
+
title: string;
|
|
14
|
+
id: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
label?: string | JSX.Element;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const RadioButtonGroup = (props: RadioButtonGroupProps) => {
|
|
21
|
+
const {
|
|
22
|
+
id,
|
|
23
|
+
className,
|
|
24
|
+
ariaHidden,
|
|
25
|
+
ariaLabel,
|
|
26
|
+
role,
|
|
27
|
+
choices,
|
|
28
|
+
selectedId,
|
|
29
|
+
onChoiceSelected
|
|
30
|
+
} = props;
|
|
31
|
+
|
|
32
|
+
const onChoiceClick = (id: string) => {
|
|
33
|
+
onChoiceSelected(id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<FocusList id={id}
|
|
38
|
+
className={classList("common-radio-group", className)}
|
|
39
|
+
ariaHidden={ariaHidden}
|
|
40
|
+
ariaLabel={ariaLabel}
|
|
41
|
+
role={role || "radiogroup"}
|
|
42
|
+
childTabStopId={selectedId}>
|
|
43
|
+
{choices.map(choice =>
|
|
44
|
+
<div key={choice.id}
|
|
45
|
+
className={classList("common-radio-choice", choice.className, selectedId === choice.id && "selected" )}
|
|
46
|
+
onClick={() => onChoiceClick(choice.id)}>
|
|
47
|
+
<input
|
|
48
|
+
type="radio"
|
|
49
|
+
id={choice.id}
|
|
50
|
+
value={choice.id}
|
|
51
|
+
name={id + "-input"}
|
|
52
|
+
checked={selectedId === choice.id}
|
|
53
|
+
tabIndex={0}
|
|
54
|
+
aria-label={choice.label ? undefined : choice.title}
|
|
55
|
+
aria-labelledby={choice.label ? choice.id + "-label" : undefined} />
|
|
56
|
+
{choice.label &&
|
|
57
|
+
<span id={choice.id + "-label"}>
|
|
58
|
+
{choice.label}
|
|
59
|
+
</span>
|
|
60
|
+
}
|
|
61
|
+
{choice.icon && <i className={choice.icon} aria-hidden={true}/>}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</FocusList>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -68,4 +68,27 @@ export enum CheckboxStatus {
|
|
|
68
68
|
Selected,
|
|
69
69
|
Unselected,
|
|
70
70
|
Waiting
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ClientCoordinates {
|
|
74
|
+
clientX: number;
|
|
75
|
+
clientY: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function clientCoord(ev: PointerEvent | MouseEvent | TouchEvent): ClientCoordinates {
|
|
79
|
+
if ((ev as TouchEvent).touches) {
|
|
80
|
+
const te = ev as TouchEvent;
|
|
81
|
+
if (te.touches.length) {
|
|
82
|
+
return te.touches[0];
|
|
83
|
+
}
|
|
84
|
+
return te.changedTouches[0];
|
|
85
|
+
}
|
|
86
|
+
return (ev as PointerEvent | MouseEvent);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function screenToSVGCoord(ref: SVGSVGElement, coord: ClientCoordinates) {
|
|
90
|
+
const screenCoord = ref.createSVGPoint();
|
|
91
|
+
screenCoord.x = coord.clientX;
|
|
92
|
+
screenCoord.y = coord.clientY;
|
|
93
|
+
return screenCoord.matrixTransform(ref.getScreenCTM().inverse());
|
|
71
94
|
}
|
|
@@ -192,6 +192,27 @@
|
|
|
192
192
|
color: @buttonMenuTextColorInverted;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/****************************************************
|
|
196
|
+
* Link Buttons *
|
|
197
|
+
****************************************************/
|
|
198
|
+
|
|
199
|
+
.common-button.link-button {
|
|
200
|
+
background: none;
|
|
201
|
+
border: none;
|
|
202
|
+
padding: 0;
|
|
203
|
+
color: @buttonLinkColor;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.common-button.link-button:hover {
|
|
207
|
+
text-decoration: underline;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.common-button.link-button:focus::after {
|
|
211
|
+
outline: none;
|
|
212
|
+
border: none;
|
|
213
|
+
text-decoration: underline;
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
/****************************************************
|
|
196
217
|
* High Contrast *
|
|
197
218
|
****************************************************/
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
.common-dropdown {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: fit-content;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.common-dropdown > .common-button {
|
|
7
|
+
display: block;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
|
|
10
|
+
color: @inputTextColor;
|
|
11
|
+
background-color: @inputBackgroundColor;
|
|
12
|
+
border: 1px solid @inputBorderColor;
|
|
13
|
+
|
|
14
|
+
min-width: 10rem;
|
|
15
|
+
border-radius: 2px;
|
|
16
|
+
padding: 0px 28px 0px 8px;
|
|
17
|
+
margin: 0px;
|
|
18
|
+
height: 32px;
|
|
19
|
+
line-height: 30px;
|
|
20
|
+
position: relative;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
text-overflow: ellipsis;
|
|
24
|
+
text-align: left;
|
|
25
|
+
|
|
26
|
+
& > .common-button-flex > i.right {
|
|
27
|
+
position: absolute;
|
|
28
|
+
right: 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&:focus::after {
|
|
32
|
+
outline: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&:focus {
|
|
36
|
+
border: 1px solid @inputBorderColorFocus;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.common-dropdown .common-button > .common-button-flex > i:first-child {
|
|
41
|
+
margin-right: 0.5rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.common-dropdown > .common-menu-dropdown-pane {
|
|
45
|
+
width: unset;
|
|
46
|
+
right: unset;
|
|
47
|
+
min-width: 100%;
|
|
48
|
+
left: 0;
|
|
49
|
+
z-index: 1;
|
|
50
|
+
|
|
51
|
+
li .common-button {
|
|
52
|
+
text-align: left;
|
|
53
|
+
width: 100%;
|
|
54
|
+
padding-left: 0.5rem;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.common-dropdown-shadow {
|
|
59
|
+
box-shadow: 0 3.2px 7.2px 0 rgb(0 0 0 ~"/ 13%"), 0 0.6px 1.8px 0 rgb(0 0 0 ~"/ 11%");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.common-dropdown.icon-preview > .common-button {
|
|
63
|
+
min-width: unset;
|
|
64
|
+
|
|
65
|
+
.common-button-label {
|
|
66
|
+
display: none;
|
|
67
|
+
}
|
|
68
|
+
}
|