diff --git a/react-common/components/controls/Button.tsx b/react-common/components/controls/Button.tsx
index 19497d70c712..8ab0d2abfb87 100644
--- a/react-common/components/controls/Button.tsx
+++ b/react-common/components/controls/Button.tsx
@@ -28,6 +28,7 @@ export interface ButtonViewProps extends ContainerProps {
export interface ButtonProps extends ButtonViewProps {
onClick: () => void;
+ onRightClick?: () => void;
onBlur?: () => void;
onKeydown?: (e: React.KeyboardEvent) => void;
}
@@ -49,6 +50,7 @@ export const Button = (props: ButtonProps) => {
ariaPressed,
role,
onClick,
+ onRightClick,
onKeydown,
onBlur,
buttonRef,
@@ -78,7 +80,8 @@ export const Button = (props: ButtonProps) => {
);
let clickHandler = (ev: React.MouseEvent) => {
- if (onClick) onClick();
+ if (onRightClick && ev.button !== 0) onRightClick();
+ else if (onClick) onClick();
if (href) window.open(href, target || "_blank", "noopener,noreferrer")
ev.stopPropagation();
ev.preventDefault();
diff --git a/react-common/components/controls/CarouselNav.tsx b/react-common/components/controls/CarouselNav.tsx
new file mode 100644
index 000000000000..4627d073ade6
--- /dev/null
+++ b/react-common/components/controls/CarouselNav.tsx
@@ -0,0 +1,62 @@
+import { classList } from "../util";
+import { Button } from "./Button";
+
+export interface CarouselNavProps {
+ pages: number;
+ selected: number;
+ maxDisplayed?: number;
+ onPageSelected: (page: number) => void;
+}
+
+export const CarouselNav = (props: CarouselNavProps) => {
+ const { pages, selected, maxDisplayed, onPageSelected } = props;
+
+ const displayedPages: number[] = [];
+ let start = 0;
+ let end = pages;
+
+ if (maxDisplayed) {
+ start = Math.min(
+ Math.max(0, selected - (maxDisplayed >> 1)),
+ Math.max(0, start + pages - maxDisplayed)
+ );
+ end = Math.min(start + maxDisplayed, pages);
+ }
+
+ for (let i = start; i < end; i++) {
+ displayedPages.push(i);
+ }
+
+ return (
+
+
+ )
+}
\ No newline at end of file
diff --git a/react-common/components/controls/FocusTrap.tsx b/react-common/components/controls/FocusTrap.tsx
deleted file mode 100644
index 39d2546c9073..000000000000
--- a/react-common/components/controls/FocusTrap.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import * as React from "react";
-import { classList, nodeListToArray, findNextFocusableElement, focusLastActive } from "../util";
-
-export interface FocusTrapProps extends React.PropsWithChildren<{}> {
- onEscape: () => void;
- id?: string;
- className?: string;
- arrowKeyNavigation?: boolean;
- dontStealFocus?: boolean;
- includeOutsideTabOrder?: boolean;
- dontRestoreFocus?: boolean;
-}
-
-export const FocusTrap = (props: FocusTrapProps) => {
- const {
- children,
- id,
- className,
- onEscape,
- arrowKeyNavigation,
- dontStealFocus,
- includeOutsideTabOrder,
- dontRestoreFocus
- } = props;
-
- let container: HTMLDivElement;
- const previouslyFocused = React.useRef(document.activeElement);
- const [stoleFocus, setStoleFocus] = React.useState(false);
-
- React.useEffect(() => {
- return () => {
- if (!dontRestoreFocus && previouslyFocused.current) {
- focusLastActive(previouslyFocused.current as HTMLElement)
- }
- }
- }, [])
-
- const getElements = () => {
- const all = nodeListToArray(
- includeOutsideTabOrder ? container.querySelectorAll(`[tabindex]`) :
- container.querySelectorAll(`[tabindex]:not([tabindex="-1"])`)
- );
-
- return all as HTMLElement[];
- }
-
- const handleRef = (ref: HTMLDivElement) => {
- if (!ref) return;
- container = ref;
-
- if (!dontStealFocus && !stoleFocus && !ref.contains(document.activeElement) && getElements().length) {
- container.focus();
-
- // Only steal focus once
- setStoleFocus(true);
- }
- }
-
- const onKeyDown = (e: React.KeyboardEvent) => {
- if (!container) return;
-
- const moveFocus = (forward: boolean, goToEnd: boolean) => {
- const focusable = getElements();
-
- if (!focusable.length) return;
-
- const index = focusable.indexOf(e.target as HTMLElement);
-
- if (forward) {
- if (goToEnd) {
- findNextFocusableElement(focusable, index, focusable.length - 1, forward).focus();
- }
- else if (index === focusable.length - 1) {
- findNextFocusableElement(focusable, index, 0, forward).focus();
- }
- else {
- findNextFocusableElement(focusable, index, index + 1, forward).focus();
- }
- }
- else {
- if (goToEnd) {
- findNextFocusableElement(focusable, index, 0, forward).focus();
- }
- else if (index === 0) {
- findNextFocusableElement(focusable, index, focusable.length - 1, forward).focus();
- }
- else {
- findNextFocusableElement(focusable, index, Math.max(index - 1, 0), forward).focus();
- }
- }
-
- e.preventDefault();
- e.stopPropagation();
- }
-
- if (e.key === "Escape") {
- onEscape();
- e.preventDefault();
- e.stopPropagation();
- }
- else if (e.key === "Tab") {
- if (e.shiftKey) moveFocus(false, false);
- else moveFocus(true, false);
- }
- else if (arrowKeyNavigation) {
- if (e.key === "ArrowDown") {
- moveFocus(true, false);
- }
- else if (e.key === "ArrowUp") {
- moveFocus(false, false);
- }
- else if (e.key === "Home") {
- moveFocus(false, true);
- }
- else if (e.key === "End") {
- moveFocus(true, true);
- }
- }
- }
-
- return
- {children}
-
-}
\ No newline at end of file
diff --git a/react-common/components/controls/FocusTrap/FocusTrap.tsx b/react-common/components/controls/FocusTrap/FocusTrap.tsx
new file mode 100644
index 000000000000..79c433a5b11c
--- /dev/null
+++ b/react-common/components/controls/FocusTrap/FocusTrap.tsx
@@ -0,0 +1,240 @@
+import * as React from "react";
+import { classList, nodeListToArray, findNextFocusableElement, focusLastActive } from "../../util";
+import { addRegion, FocusTrapProvider, removeRegion, useFocusTrapDispatch, useFocusTrapState } from "./context";
+import { useId } from "../../../hooks/useId";
+
+export interface FocusTrapProps extends React.PropsWithChildren<{}> {
+ onEscape: () => void;
+ id?: string;
+ className?: string;
+ arrowKeyNavigation?: boolean;
+ dontStealFocus?: boolean;
+ includeOutsideTabOrder?: boolean;
+ dontRestoreFocus?: boolean;
+}
+
+export const FocusTrap = (props: FocusTrapProps) => {
+ return (
+
+
+
+ );
+}
+
+const FocusTrapInner = (props: FocusTrapProps) => {
+ const {
+ children,
+ id,
+ className,
+ onEscape,
+ arrowKeyNavigation,
+ dontStealFocus,
+ includeOutsideTabOrder,
+ dontRestoreFocus
+ } = props;
+
+ let container: HTMLDivElement;
+ const previouslyFocused = React.useRef(document.activeElement);
+ const [stoleFocus, setStoleFocus] = React.useState(false);
+
+ const { regions } = useFocusTrapState();
+
+ React.useEffect(() => {
+ return () => {
+ if (!dontRestoreFocus && previouslyFocused.current) {
+ focusLastActive(previouslyFocused.current as HTMLElement)
+ }
+ }
+ }, [])
+
+ const getElements = React.useCallback(() => {
+ let all = nodeListToArray(
+ includeOutsideTabOrder ? container.querySelectorAll(`[tabindex]`) :
+ container.querySelectorAll(`[tabindex]:not([tabindex="-1"])`)
+ );
+
+ if (regions.length) {
+ const regionElements: pxt.Map = {};
+
+ for (const region of regions) {
+ const el = container.querySelector(`[data-focus-trap-region="${region.id}"]`);
+
+ if (el) {
+ regionElements[region.id] = el;
+ }
+ }
+
+ for (const region of regions) {
+ const regionElement = regionElements[region.id];
+ if (!region.enabled && regionElement) {
+ all = all.filter(el => !regionElement.contains(el));
+ }
+ }
+
+ const initialOrder = all.slice();
+ all.sort((a, b) => {
+ const aRegion = regions.find(r => r.enabled && regionElements[r.id]?.contains(a));
+ const bRegion = regions.find(r => r.enabled && regionElements[r.id]?.contains(b));
+
+ if (aRegion?.order === bRegion?.order) {
+ const aIndex = initialOrder.indexOf(a);
+ const bIndex = initialOrder.indexOf(b);
+ return aIndex - bIndex;
+ }
+ else if (!aRegion) {
+ return 1;
+ }
+ else if (!bRegion) {
+ return -1;
+ }
+ else {
+ return aRegion.order - bRegion.order;
+ }
+ });
+ }
+
+ return all as HTMLElement[];
+ }, [regions, includeOutsideTabOrder]);
+
+ const handleRef = React.useCallback((ref: HTMLDivElement) => {
+ if (!ref) return;
+ container = ref;
+
+ if (!dontStealFocus && !stoleFocus && !ref.contains(document.activeElement) && getElements().length) {
+ container.focus();
+
+ // Only steal focus once
+ setStoleFocus(true);
+ }
+ }, [getElements, dontStealFocus, stoleFocus]);
+
+ const onKeyDown = React.useCallback((e: React.KeyboardEvent) => {
+ if (!container) return;
+
+ const moveFocus = (forward: boolean, goToEnd: boolean) => {
+ const focusable = getElements();
+
+ if (!focusable.length) return;
+
+ const index = focusable.indexOf(e.target as HTMLElement);
+
+ if (forward) {
+ if (goToEnd) {
+ findNextFocusableElement(focusable, index, focusable.length - 1, forward).focus();
+ }
+ else if (index === focusable.length - 1) {
+ findNextFocusableElement(focusable, index, 0, forward).focus();
+ }
+ else {
+ findNextFocusableElement(focusable, index, index + 1, forward).focus();
+ }
+ }
+ else {
+ if (goToEnd) {
+ findNextFocusableElement(focusable, index, 0, forward).focus();
+ }
+ else if (index === 0) {
+ findNextFocusableElement(focusable, index, focusable.length - 1, forward).focus();
+ }
+ else {
+ findNextFocusableElement(focusable, index, Math.max(index - 1, 0), forward).focus();
+ }
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ if (e.key === "Escape") {
+ let foundHandler = false;
+ if (regions.length) {
+ for (const region of regions) {
+ if (!region.onEscape) continue;
+ const regionElement = container.querySelector(`[data-focus-trap-region="${region.id}"]`);
+ if (regionElement?.contains(document.activeElement)) {
+ foundHandler = true;
+ region.onEscape();
+ break;
+ }
+ }
+ }
+ if (!foundHandler) {
+ onEscape();
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ else if (e.key === "Tab") {
+ if (e.shiftKey) moveFocus(false, false);
+ else moveFocus(true, false);
+ }
+ else if (arrowKeyNavigation) {
+ if (e.key === "ArrowDown") {
+ moveFocus(true, false);
+ }
+ else if (e.key === "ArrowUp") {
+ moveFocus(false, false);
+ }
+ else if (e.key === "Home") {
+ moveFocus(false, true);
+ }
+ else if (e.key === "End") {
+ moveFocus(true, true);
+ }
+ }
+ }, [getElements, onEscape, arrowKeyNavigation, regions])
+
+ return(
+
+ {children}
+
+ );
+}
+
+
+
+interface FocusTrapRegionProps extends React.PropsWithChildren<{}> {
+ enabled: boolean;
+ order?: number;
+ onEscape?: () => void;
+ id?: string;
+ className?: string;
+ divRef?: (ref: HTMLDivElement) => void;
+}
+
+export const FocusTrapRegion = (props: FocusTrapRegionProps) => {
+ const {
+ className,
+ id,
+ onEscape,
+ order,
+ enabled,
+ children,
+ divRef
+ } = props;
+
+ const regionId = useId();
+ const dispatch = useFocusTrapDispatch();
+
+ React.useEffect(() => {
+ dispatch(addRegion(regionId, order, enabled, onEscape));
+
+ return () => dispatch(removeRegion(regionId));
+ }, [regionId, enabled, order])
+
+ return (
+
+ {children}
+
+ )
+}
\ No newline at end of file
diff --git a/react-common/components/controls/FocusTrap/context.tsx b/react-common/components/controls/FocusTrap/context.tsx
new file mode 100644
index 000000000000..5f3f47e70404
--- /dev/null
+++ b/react-common/components/controls/FocusTrap/context.tsx
@@ -0,0 +1,103 @@
+import * as React from "react";
+
+interface FocusTrapState {
+ regions: FocusTrapRegionState[];
+}
+
+interface FocusTrapRegionState {
+ id: string;
+ enabled: boolean;
+ order: number;
+ onEscape?: () => void;
+}
+
+const FocustTrapStateContext = React.createContext(null);
+const FocustTrapDispatchContext = React.createContext<(action: Action) => void>(null);
+
+export const FocusTrapProvider = ({
+ children,
+}: React.PropsWithChildren<{}>) => {
+ const [state, dispatch] = React.useReducer(focusTrapReducer, {
+ regions: []
+ });
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+type AddRegion = {
+ type: "ADD_REGION";
+ id: string;
+ order: number;
+ enabled: boolean;
+ onEscape?: () => void;
+};
+
+type RemoveRegion = {
+ type: "REMOVE_REGION";
+ id: string;
+};
+
+type Action = AddRegion | RemoveRegion;
+
+export const addRegion = (id: string, order: number, enabled: boolean, onEscape?: () => void): AddRegion => (
+ {
+ type: "ADD_REGION",
+ id,
+ order,
+ enabled,
+ onEscape
+ }
+);
+
+export const removeRegion = (id: string): RemoveRegion => (
+ {
+ type: "REMOVE_REGION",
+ id
+ }
+);
+
+export function useFocusTrapState() {
+ return React.useContext(FocustTrapStateContext);
+}
+
+export function useFocusTrapDispatch() {
+ return React.useContext(FocustTrapDispatchContext);
+}
+
+function focusTrapReducer(state: FocusTrapState, action: Action): FocusTrapState {
+ let newRegions = state.regions.slice();
+
+ switch (action.type) {
+ case "ADD_REGION":
+ const newRegion = {
+ id: action.id,
+ enabled: action.enabled,
+ order: action.order,
+ onEscape: action.onEscape
+ };
+ const existing = newRegions.findIndex(r => r.id === action.id);
+ if (existing !== -1) {
+ newRegions.splice(existing, 1, newRegion)
+ }
+ else {
+ newRegions.push(newRegion);
+ }
+ break;
+ case "REMOVE_REGION":
+ const toRemove = state.regions.findIndex(r => r.id === action.id);
+ if (toRemove !== -1) {
+ newRegions.splice(toRemove, 1)
+ }
+ break;
+ }
+
+ return {
+ regions: newRegions
+ };
+}
diff --git a/react-common/components/controls/FocusTrap/index.ts b/react-common/components/controls/FocusTrap/index.ts
new file mode 100644
index 000000000000..a51d81ad6b6b
--- /dev/null
+++ b/react-common/components/controls/FocusTrap/index.ts
@@ -0,0 +1 @@
+export * from "./FocusTrap";
\ No newline at end of file
diff --git a/react-common/styles/controls/CarouselNav.less b/react-common/styles/controls/CarouselNav.less
new file mode 100644
index 000000000000..0f720e04ee47
--- /dev/null
+++ b/react-common/styles/controls/CarouselNav.less
@@ -0,0 +1,77 @@
+.common-carousel-nav {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+
+ .common-carousel-nav-arrow {
+ min-height: 1rem;
+ min-width: 1rem;
+ padding: 0;
+ background: none;
+ color: white;
+ width: 1.25rem;
+ height: 1.25rem;
+
+ margin: 0;
+
+ i.fas {
+ width: 1.25rem;
+ }
+
+ &.disabled {
+ opacity: 0.5;
+ }
+ }
+
+ ul {
+ list-style: none;
+ margin: 0;
+ margin-left: 0.125rem;
+ margin-right: 0.125rem;
+ margin-block: 0;
+ padding-inline: 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: 1.25rem;
+ }
+
+ li {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: 1rem;
+ width: 1rem;
+
+ .common-button {
+ width: 1rem;
+ height: 1rem;
+ padding: 0.3rem;
+ background: none;
+
+ .common-carousel-nav-button-handle {
+ background-color: white;
+ border-radius: 100%;
+ height: 0.4rem;
+ width: 0.4rem;
+ display: block;
+ }
+
+ &.selected {
+ padding: 0.125rem;
+
+ .common-carousel-nav-button-handle {
+ height: 0.75rem;
+ width: 0.75rem;
+ }
+ }
+ }
+ }
+
+ .common-button:focus-visible::after {
+ inset: -1px;
+ }
+}
diff --git a/react-common/styles/react-common.less b/react-common/styles/react-common.less
index 87e9debe8bdb..bef6d8cabacc 100644
--- a/react-common/styles/react-common.less
+++ b/react-common/styles/react-common.less
@@ -25,6 +25,7 @@
@import "controls/VerticalResizeContainer.less";
@import "controls/VerticalSlider.less";
@import "controls/Accordion.less";
+@import "controls/CarouselNav.less";
@import "./react-common-variables.less";
@import "fontawesome-free/less/solid.less";
diff --git a/theme/image-editor/button.less b/theme/image-editor/button.less
index 2a04a107c56f..c6061cd5c7ec 100644
--- a/theme/image-editor/button.less
+++ b/theme/image-editor/button.less
@@ -1,20 +1,26 @@
-.image-editor-button {
- text-align: center;
+.common-button.image-editor-button {
line-height: 2rem;
width: 1.75rem;
height: 1.75rem;
border-radius: 0.25rem;
margin: 0.375rem;
+ padding: 0;
+ min-width: unset;
+ min-height: unset;
transition: color 0.1s;
color: var(--sidebar-icon-active-color);
+ background: none;
- cursor: pointer;
- user-select: none;
+ &:focus-visible::after {
+ inset: -1px;
+ outline-color: @inputBorderColorFocus;
+ }
}
-.image-editor-button.toggle, .image-editor-button.disabled {
+.image-editor-button.toggle, .common-button.image-editor-button.disabled {
color: var(--sidebar-icon-inactive-color);
+ background: none;
}
.image-editor-button.disabled {
diff --git a/theme/image-editor/cursorSizes.less b/theme/image-editor/cursorSizes.less
index c906c4050879..62c65491c0ce 100644
--- a/theme/image-editor/cursorSizes.less
+++ b/theme/image-editor/cursorSizes.less
@@ -3,13 +3,21 @@
display: flex;
flex-direction: row;
margin: auto;
+ text-align: center;
}
.cursor-button {
border-radius: 1px;
- background-color: var(--sidebar-icon-inactive-color);
+ background-color: var(--sidebar-icon-active-color);
margin-right: 0.25rem;
transition: background-color 0.1s;
+ vertical-align: middle;
+ display: inline-block;
+ margin-top: -0.25rem;
+}
+
+.toggle .cursor-button {
+ background-color: var(--sidebar-icon-inactive-color);
}
.cursor-button-outer {
@@ -17,24 +25,21 @@
width: 1.5rem;
}
-.cursor-button-outer:hover .cursor-button, .cursor-button-outer.selected .cursor-button {
+.common-button:hover .cursor-button {
background-color: var(--sidebar-icon-active-color);
}
.cursor-button.small {
- margin-top: 0.75rem;
width: 0.5rem;
height: 0.5rem;
}
.cursor-button.medium {
- margin-top: 0.625rem;
width: 0.75rem;
height: 0.75rem;
}
.cursor-button.large {
- margin-top: 0.5rem;
width: 1rem;
height: 1rem;
}
\ No newline at end of file
diff --git a/theme/image-editor/imageCanvas.less b/theme/image-editor/imageCanvas.less
index c3dd8dfe49f5..a9d83e36adaa 100644
--- a/theme/image-editor/imageCanvas.less
+++ b/theme/image-editor/imageCanvas.less
@@ -48,7 +48,7 @@
-ms-interpolation-mode: nearest-neighbor;
}
-.checkerboard {
+.checkerboard, .common-button.image-editor-button.checkerboard {
background-color: #aeaeae;
background-image: linear-gradient(45deg, #dedede 25%, transparent 25%), linear-gradient(-45deg, #dedede 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #dedede 75%), linear-gradient(-45deg, transparent 75%, #dedede 75%);
background-size: 0.75rem 0.75rem;
diff --git a/theme/image-editor/imageEditor.less b/theme/image-editor/imageEditor.less
index becb3be9cd8e..f6be06ae8aa9 100644
--- a/theme/image-editor/imageEditor.less
+++ b/theme/image-editor/imageEditor.less
@@ -90,12 +90,19 @@
overflow: hidden;
}
+.image-editor-region {
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
.gallery-editor-header {
height: 3rem;
background-color: #4B7BEC;
border: 2px solid #4067b3;
border-bottom: none;
display: flex;
+ flex-shrink: 0;
}
.image-editor-header-left,
@@ -292,7 +299,7 @@
max-width: 120px;
}
-.image-editor-confirm {
+.common-button.image-editor-confirm {
display: flex;
align-items: center;
padding: 0 2rem;
@@ -300,13 +307,15 @@
color: @white;
cursor: pointer;
user-select: none;
+ margin: 0;
+ border-radius: 0;
}
-.image-editor-confirm:hover {
+.common-button.image-editor-confirm:hover {
background-color: darken(@green, 5%);
}
-.image-editor-confirm:active {
+.common-button.image-editor-confirm:active {
background-color: darken(@green, 10%);
}
diff --git a/theme/image-editor/tilePalette.less b/theme/image-editor/tilePalette.less
index b8c464696841..3e907842310c 100644
--- a/theme/image-editor/tilePalette.less
+++ b/theme/image-editor/tilePalette.less
@@ -105,74 +105,116 @@
.tile-canvas-controls {
width: 100%;
text-align: center;
- margin-top: -0.5rem;
}
-.tile-palette-pages {
- height: 1.75rem;
- display: inline-block;
+.tile-canvas {
+ width: 9.5rem;
+ height: 9.5rem;
+ position: relative;
+
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-rows: repeat(4, 1fr);
+ gap: 1px;
+
+ margin: 0.25rem;
+ padding: 2px;
+ background-color: #3d3d3d;
}
-.tile-palette-page-arrow {
- fill: var(--sidebar-icon-inactive-color);
- stroke: var(--sidebar-icon-inactive-color);
- stroke-linejoin: round;
- cursor: pointer;
+.tile-palette-controls {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ align-content: center;
+ height: 2rem;
+}
- transition: fill 0.1s, stroke 0.1s;
+.tile-palette-controls .image-editor-button {
+ line-height: 1.75rem;
}
-.tile-palette-page-dot {
- fill: var(--sidebar-icon-inactive-color);
- cursor: pointer;
- transition: fill 0.1s;
+.tile-palette-controls-outer {
+ height: 2rem;
}
-.tile-palette-pages:not(.disabled) {
- .tile-palette-page-arrow:hover {
- fill: var(--sidebar-icon-active-color);
- stroke: var(--sidebar-icon-active-color);
+.tile-canvas-controls .common-carousel-nav {
+ .common-carousel-nav-arrow {
+ color: var(--sidebar-icon-inactive-color);
+ transition: color 0.1s;
+
+ &:hover {
+ color: var(--sidebar-icon-active-color);
+ filter: none;
+ }
}
- .tile-palette-page-dot:hover {
- fill: var(--sidebar-icon-active-color);
+ li .common-button {
+ .common-carousel-nav-button-handle {
+ background-color: var(--sidebar-icon-inactive-color);
+ transition: background-color 0.1s;
+ }
+
+ &:hover {
+ filter: none;
+
+ .common-carousel-nav-button-handle {
+ background-color: var(--sidebar-icon-active-color);
+ }
+ }
}
}
-.tile-palette-pages.disabled {
- .tile-palette-page-arrow,
- .tile-palette-page-dot {
- opacity: 0.5;
+.tile-palette-dropdown.common-dropdown {
+ .common-dropdown-button {
+ background: none;
+ color: var(--sidebar-icon-active-color);
+ border: 1px solid var(--sidebar-icon-inactive-color);
+ min-width: unset;
+ width: calc(10rem - 0.5rem);
+ margin: 0 0.25rem;
+ transition: border-color 0.1s;
+
+ &:hover {
+ border-color: var(--sidebar-icon-active-color);
+ filter: none;
+ }
+ }
+
+ .common-menu-dropdown-pane {
+ max-height: 12rem;
+ overflow-y: auto;
+ overflow-x: hidden;
}
}
-.tile-canvas {
- width: 100%;
- padding: 0.25rem;
+.tile-button-outer {
position: relative;
}
-.tile-canvas .paint-surface {
+
+.image-editor-button.common-button.tile-button {
+ margin: 0;
width: 100%;
- background-color: #3d3d3d;
-}
+ height: 100%;
+ background-color: var(--editing-tools-bg-color);
+ border-radius: 0;
-.tile-palette-controls {
- display: flex;
- flex-direction: row;
- justify-content: space-evenly;
- align-content: center;
- height: 2rem;
-}
+ canvas {
+ width: 100%;
+ height: 100%;
-.tile-palette-controls .image-editor-button {
- line-height: 1.75rem;
+ image-rendering: pixelated;
+ }
}
-.tile-palette-controls-outer {
- height: 2rem;
-}
+.image-editor-button.common-button.add-tile-button {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+ margin-top: 0.2rem;
+}
@media screen and (max-height: 720px) {
.image-editor-tilemap-minimap {
diff --git a/theme/image-editor/timeline.less b/theme/image-editor/timeline.less
index fb51866f1ce8..f1f55f30522f 100644
--- a/theme/image-editor/timeline.less
+++ b/theme/image-editor/timeline.less
@@ -7,6 +7,13 @@
flex-direction: column;
}
+.image-editor-timeline-frames {
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ padding: 0.4rem 0;
+}
+
.image-editor-timeline-frame {
border: 1px var(--sidebar-icon-inactive-color) solid;
color: var(--sidebar-icon-inactive-color);
@@ -14,7 +21,7 @@
border-radius: 0.25rem;
width: 6rem;
height: 6rem;
- margin: 0.4rem;
+ margin: 0 0.4rem;
transition: border 0.2s, color 0.2s, background-color 0.2s;
overflow: hidden;
@@ -126,6 +133,20 @@
background-color: #aeaeae;
}
+.common-button.image-editor-button.add-frame-button {
+ height: 2rem;
+ width: 6rem;
+ margin: 0 0.4rem;
+
+ border: 1px solid var(--sidebar-icon-inactive-color);
+ transition: color 0.1s, border-color 0.1s;
+
+ &:hover {
+ border-color: var(--sidebar-icon-active-color);
+ filter: none;
+ }
+}
+
/* .timeline-frame-actions */
diff --git a/theme/image-editor/topBar.less b/theme/image-editor/topBar.less
index 0ee9874f5611..5aebbe8e1afc 100644
--- a/theme/image-editor/topBar.less
+++ b/theme/image-editor/topBar.less
@@ -9,6 +9,7 @@
.image-editor-topbar > div .image-editor-button {
margin-top: 0;
margin-left: 0;
+ height: 2rem;
}
.image-editor-topbar > div {
diff --git a/webapp/src/components/ImageEditor/Alert.tsx b/webapp/src/components/ImageEditor/Alert.tsx
index d15bb9e858e6..cd69e5c7ae8b 100644
--- a/webapp/src/components/ImageEditor/Alert.tsx
+++ b/webapp/src/components/ImageEditor/Alert.tsx
@@ -2,7 +2,6 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { ImageEditorStore } from './store/imageReducer';
import { dispatchHideAlert } from './actions/dispatch';
-import { IconButton } from "./Button";
export interface AlertOption {
label: string;
diff --git a/webapp/src/components/ImageEditor/BottomBar.tsx b/webapp/src/components/ImageEditor/BottomBar.tsx
index 8107117e47c1..cb2eb84c7859 100644
--- a/webapp/src/components/ImageEditor/BottomBar.tsx
+++ b/webapp/src/components/ImageEditor/BottomBar.tsx
@@ -3,11 +3,11 @@ import * as React from "react";
import { connect } from 'react-redux';
import { ImageEditorStore, AnimationState, TilemapState } from './store/imageReducer';
import { dispatchChangeImageDimensions, dispatchUndoImageEdit, dispatchRedoImageEdit, dispatchToggleAspectRatioLocked, dispatchChangeZoom, dispatchToggleOnionSkinEnabled, dispatchChangeAssetName } from './actions/dispatch';
-import { IconButton } from "./Button";
import { fireClickOnlyOnEnter } from "./util";
import { isNameTaken } from "../../assets";
import { obtainShortcutLock, releaseShortcutLock } from "./keyboardShortcuts";
import { classList } from "../../../../react-common/components/util";
+import { Button } from "../../../../react-common/components/controls/Button";
export interface BottomBarProps {
dispatchChangeImageDimensions: (dimensions: [number, number]) => void;
@@ -93,12 +93,11 @@ export class BottomBarImpl extends React.Component
-
}
{ !singleFrame &&
-
}
{ !resizeDisabled && }
@@ -145,42 +144,44 @@ export class BottomBarImpl extends React.Component
-
-
-
-
- {!hideDoneButton &&
- {lf("Done")}
-
}
+ {!hideDoneButton &&
+
+ }
);
}
diff --git a/webapp/src/components/ImageEditor/CursorSizes.tsx b/webapp/src/components/ImageEditor/CursorSizes.tsx
index dfece1f1702f..87ad80f6ac6c 100644
--- a/webapp/src/components/ImageEditor/CursorSizes.tsx
+++ b/webapp/src/components/ImageEditor/CursorSizes.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
import { ImageEditorStore, CursorSize } from './store/imageReducer';
import { dispatchChangeCursorSize } from './actions/dispatch';
import { connect } from 'react-redux';
+import { Button } from '../../../../react-common/components/controls/Button';
+import { classList } from '../../../../react-common/components/util';
interface CursorSizesProps {
selected: CursorSize;
@@ -14,17 +16,28 @@ class CursorSizesImpl extends React.Component {
render() {
const { selected } = this.props;
- return
-
-
+ return (
+
+ }
+ onClick={this.clickHandler(CursorSize.One)}
+ />
+ }
+ onClick={this.clickHandler(CursorSize.Three)}
+ />
+ }
+ onClick={this.clickHandler(CursorSize.Five)}
+ />
-
-
-
+ );
}
clickHandler(size: CursorSize) {
diff --git a/webapp/src/components/ImageEditor/Dropdown.tsx b/webapp/src/components/ImageEditor/Dropdown.tsx
deleted file mode 100644
index aa351b69be0e..000000000000
--- a/webapp/src/components/ImageEditor/Dropdown.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import * as React from 'react';
-
-export interface DropdownOption {
- text: string;
- id: string;
-}
-
-export interface DropdownProps {
- options: DropdownOption[];
- selected: number;
- onChange: (selected: DropdownOption, index: number) => void;
-}
-
-export interface DropdownState {
- open: boolean;
-}
-
-export class Dropdown extends React.Component
{
- protected handlers: (() => void)[] = [];
-
- constructor(props: DropdownProps) {
- super(props);
-
- this.state = {
- open: false
- };
- }
-
- componentDidUpdate() {
- this.handlers = [];
- }
-
- componentWillUnmount() {
- this.handlers = null;
- }
-
- render() {
- const { options, selected } = this.props;
- const { open } = this.state;
-
- const selectedOption = options[selected];
-
- return
-
- { selectedOption?.text || "" }
-
-
-
-
- {
- options.map((option, index) =>
- -
- {option.text}
-
- )
- }
-
-
- }
-
- protected clickHandler(index: number): () => void {
- if (!this.handlers[index]) {
- const { onChange, options } = this.props;
-
- this.handlers[index] = () => {
- this.setState({ open: false });
- onChange(options[index], index);
- };
- }
-
- return this.handlers[index];
- }
-
- protected handleDropdownClick = () => {
- this.setState({ open: !this.state.open });
- }
-}
\ No newline at end of file
diff --git a/webapp/src/components/ImageEditor/SideBar.tsx b/webapp/src/components/ImageEditor/SideBar.tsx
index 9c3ab4a456a5..640308759cbb 100644
--- a/webapp/src/components/ImageEditor/SideBar.tsx
+++ b/webapp/src/components/ImageEditor/SideBar.tsx
@@ -2,12 +2,13 @@ import * as React from "react";
import { connect } from "react-redux";
import { tools } from "./toolDefinitions";
-import { IconButton } from "./Button";
import { ImageEditorTool, ImageEditorStore } from "./store/imageReducer";
import { dispatchChangeImageTool } from "./actions/dispatch";
import { Palette } from "./sprite/Palette";
import { TilePalette } from "./tilemap/TilePalette";
import { Minimap } from "./tilemap/Minimap";
+import { Button } from "../../../../react-common/components/controls/Button";
+import { classList } from "../../../../react-common/components/util";
interface SideBarProps {
selectedTool: ImageEditorTool;
@@ -30,12 +31,13 @@ export class SideBarImpl extends React.Component {
}
{tools.filter(td => !td.hiddenTool).map(td =>
-
+ onClick={this.clickHandler(td.tool)}
+ />
)}
diff --git a/webapp/src/components/ImageEditor/Timeline.tsx b/webapp/src/components/ImageEditor/Timeline.tsx
index 2d7462d48721..2928bdb81eac 100644
--- a/webapp/src/components/ImageEditor/Timeline.tsx
+++ b/webapp/src/components/ImageEditor/Timeline.tsx
@@ -6,6 +6,7 @@ import { dispatchChangeCurrentFrame, dispatchNewFrame, dispatchDuplicateFrame, d
import { TimelineFrame } from "./TimelineFrame";
import { bindGestureEvents, ClientCoordinates } from "./util";
+import { Button } from "../../../../react-common/components/controls/Button";
interface TimelineProps {
colors: string[];
@@ -81,9 +82,12 @@ export class TimelineImpl extends React.Component
deleteFrame={this.deleteFrame} />
}
-
-
-
+
diff --git a/webapp/src/components/ImageEditor/TimelineFrame.tsx b/webapp/src/components/ImageEditor/TimelineFrame.tsx
index 9d2107b35759..47859dd50c74 100644
--- a/webapp/src/components/ImageEditor/TimelineFrame.tsx
+++ b/webapp/src/components/ImageEditor/TimelineFrame.tsx
@@ -1,5 +1,5 @@
import * as React from "react";
-import { IconButton } from "./Button";
+import { Button } from "../../../../react-common/components/controls/Button";
interface TimelineFrameProps {
frames: pxt.sprite.ImageState[];
@@ -47,13 +47,15 @@ export class TimelineFrame extends React.Component
{showActions &&
-
-
diff --git a/webapp/src/components/ImageEditor/TopBar.tsx b/webapp/src/components/ImageEditor/TopBar.tsx
index 2f36a4d73369..984a9b28c531 100644
--- a/webapp/src/components/ImageEditor/TopBar.tsx
+++ b/webapp/src/components/ImageEditor/TopBar.tsx
@@ -3,10 +3,10 @@ import * as React from "react";
import { connect } from 'react-redux';
import { ImageEditorStore, AnimationState } from './store/imageReducer';
import { dispatchChangeInterval, dispatchChangePreviewAnimating, dispatchChangeOverlayEnabled } from './actions/dispatch';
-import { IconButton } from "./Button";
import { CursorSizes } from "./CursorSizes";
import { Toggle } from "./Toggle";
import { flip, rotate } from "./keyboardShortcuts";
+import { Button } from "../../../../react-common/components/controls/Button";
export interface TopBarProps {
@@ -41,20 +41,40 @@ export class TopBarImpl extends React.Component {
-
-
-
-
+
+
+
+
{ !singleFrame && }
{ !singleFrame &&
-
diff --git a/webapp/src/components/ImageEditor/sprite/Palette.tsx b/webapp/src/components/ImageEditor/sprite/Palette.tsx
index 43da36da74f7..606fa1c0a60c 100644
--- a/webapp/src/components/ImageEditor/sprite/Palette.tsx
+++ b/webapp/src/components/ImageEditor/sprite/Palette.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { ImageEditorStore, AnimationState } from '../store/imageReducer';
import { dispatchChangeSelectedColor, dispatchChangeBackgroundColor, dispatchSwapBackgroundForeground } from '../actions/dispatch';
+import { Button } from '../../../../../react-common/components/controls/Button';
+import { classList } from '../../../../../react-common/components/util';
export interface PaletteProps {
colors: string[];
@@ -13,8 +15,6 @@ export interface PaletteProps {
}
class PaletteImpl extends React.Component
{
- protected handlers: ((ev: React.MouseEvent) => void)[] = [];
-
render() {
const { colors, selected, backgroundColor, dispatchSwapBackgroundForeground } = this.props;
const SPACER = 1;
@@ -57,33 +57,20 @@ class PaletteImpl extends React.Component {
- {this.props.colors.map((color, index) => {
- return
+
- })}
+ style={index === 0 ? null : { backgroundColor: color }}
+ onClick={() => this.props.dispatchChangeSelectedColor(index)}
+ onRightClick={() => this.props.dispatchChangeBackgroundColor(index)}
+ />
+ )}
;
}
- protected clickHandler(index: number) {
- if (!this.handlers[index]) this.handlers[index] = (ev: React.MouseEvent) => {
- if (ev.button === 0) {
- this.props.dispatchChangeSelectedColor(index);
- }
- else {
- this.props.dispatchChangeBackgroundColor(index);
- ev.preventDefault();
- ev.stopPropagation();
- }
- }
-
- return this.handlers[index];
- }
-
protected preventContextMenu = (ev: React.MouseEvent) => ev.preventDefault();
}
diff --git a/webapp/src/components/ImageEditor/tilemap/TilePalette.tsx b/webapp/src/components/ImageEditor/tilemap/TilePalette.tsx
index 8d1493f584ea..a8746c319dea 100644
--- a/webapp/src/components/ImageEditor/tilemap/TilePalette.tsx
+++ b/webapp/src/components/ImageEditor/tilemap/TilePalette.tsx
@@ -6,12 +6,15 @@ import { dispatchChangeSelectedColor, dispatchChangeBackgroundColor, dispatchSwa
dispatchCreateNewTile, dispatchSetGalleryOpen, dispatchOpenTileEditor, dispatchDeleteTile,
dispatchShowAlert, dispatchHideAlert } from '../actions/dispatch';
import { TimelineFrame } from '../TimelineFrame';
-import { Dropdown, DropdownOption } from '../Dropdown';
import { Pivot, PivotOption } from '../Pivot';
-import { IconButton } from '../Button';
import { AlertOption } from '../Alert';
import { createTile } from '../../../assets';
+import { CarouselNav } from "../../../../../react-common/components/controls/CarouselNav";
+import { Dropdown, DropdownItem } from '../../../../../react-common/components/controls/Dropdown';
+import { Button } from '../../../../../react-common/components/controls/Button';
+import { classList } from '../../../../../react-common/components/util';
+
export interface TilePaletteProps {
colors: string[];
tileset: pxt.TileSet;
@@ -49,7 +52,9 @@ const SCALE = pxt.BrowserUtils.isEdge() ? 25 : 1;
const TILES_PER_PAGE = 16;
-interface Category extends DropdownOption {
+interface Category {
+ id: string;
+ text: string;
tiles: GalleryTile[];
}
@@ -93,7 +98,6 @@ interface UserTile {
type RenderedTile = GalleryTile | UserTile
class TilePaletteImpl extends React.Component {
- protected canvas: HTMLCanvasElement;
protected renderedTiles: RenderedTile[];
protected categoryTiles: RenderedTile[];
protected categories: Category[];
@@ -129,9 +133,7 @@ class TilePaletteImpl extends React.Component {
}
componentDidMount() {
- this.canvas = this.refs["tile-canvas-surface"] as HTMLCanvasElement;
this.updateGalleryTiles();
- this.redrawCanvas();
}
UNSAFE_componentWillReceiveProps(nextProps: TilePaletteProps) {
@@ -145,7 +147,6 @@ class TilePaletteImpl extends React.Component {
componentDidUpdate() {
this.updateGalleryTiles();
- this.redrawCanvas();
}
render() {
@@ -164,6 +165,19 @@ class TilePaletteImpl extends React.Component {
const showCreateTile = !galleryOpen && (totalPages === 1 || page === totalPages - 1);
const controlsDisabled = galleryOpen || !this.renderedTiles.some(t => !isGalleryTile(t) && t.index === selected);
+ const columns = 4;
+ const rows = 4;
+ const startIndex = page * columns * rows;
+
+ const visibleTiles = this.categoryTiles.slice(startIndex, startIndex + columns * rows);
+
+ const dropdownItems: DropdownItem[] = this.categories.filter(c => !!c.tiles.length)
+ .map(cat => ({
+ id: cat.id,
+ title: cat.text,
+ label: cat.text,
+ }));
+
return
@@ -175,10 +189,12 @@ class TilePaletteImpl extends React.Component {
-
+ onClick={this.foregroundBackgroundClickHandler}
+ />
{
- { galleryOpen &&
!!c.tiles.length)} selected={category} /> }
+ { galleryOpen &&
+
+ }
{ !galleryOpen &&
-
-
-
}
@@ -222,20 +246,34 @@ class TilePaletteImpl extends React.Component {
-
+ { visibleTiles.map((tile, index) =>
+
this.handleTileClick(index, false)}
+ onRightClick={() => this.handleTileClick(index, true)}
+ />
+ )}
{ showCreateTile &&
-
-
+
}
- { pageControls(totalPages, page, this.pageHandler) }
+
;
@@ -281,65 +319,8 @@ class TilePaletteImpl extends React.Component
{
}
}
- protected redrawCanvas() {
- const columns = 4;
- const rows = 4;
- const margin = 1;
-
- const { tileset, page, selected } = this.props;
-
- const startIndex = page * columns * rows;
-
- const width = tileset.tileWidth + margin;
-
- this.canvas.width = (width * columns + margin) * SCALE;
- this.canvas.height = (width * rows + margin) * SCALE;
-
- const context = this.canvas.getContext("2d");
-
- for (let r = 0; r < rows; r++) {
- for (let c = 0; c < columns; c++) {
- const tile = this.categoryTiles[startIndex + r * columns + c];
-
- if (tile) {
- if (!isGalleryTile(tile) && tile.index === selected) {
- context.fillStyle = "#ff0000";
- context.fillRect(c * width, r * width, width + 1, width + 1);
- }
-
- context.fillStyle = "#333333";
- context.fillRect(c * width + 1, r * width + 1, width - 1, width - 1);
-
- this.drawBitmap(pxt.sprite.Bitmap.fromData(tile.bitmap), 1 + c * width, 1 + r * width)
- }
- }
- }
-
- this.positionCreateTileButton();
- }
-
- protected drawBitmap(bitmap: pxt.sprite.Bitmap, x0 = 0, y0 = 0, transparent = true, cellWidth = SCALE, target = this.canvas) {
- const { colors } = this.props;
-
- const context = target.getContext("2d");
- context.imageSmoothingEnabled = false;
- for (let x = 0; x < bitmap.width; x++) {
- for (let y = 0; y < bitmap.height; y++) {
- const index = bitmap.get(x, y);
-
- if (index) {
- context.fillStyle = colors[index];
- context.fillRect((x + x0) * cellWidth, (y + y0) * cellWidth, cellWidth, cellWidth);
- }
- else {
- if (!transparent) context.clearRect((x + x0) * cellWidth, (y + y0) * cellWidth, cellWidth, cellWidth);
- }
- }
- }
- }
-
- protected dropdownHandler = (option: DropdownOption, index: number) => {
- this.props.dispatchChangeTilePaletteCategory(index);
+ protected dropdownHandler = (id: string) => {
+ this.props.dispatchChangeTilePaletteCategory(this.categories.filter(c => !!c.tiles.length).findIndex(c => c.id === id));
}
protected pivotHandler = (option: PivotOption, index: number) => {
@@ -401,20 +382,9 @@ class TilePaletteImpl extends React.Component {
}
}
- protected canvasClickHandler = (ev: React.MouseEvent) => {
- this.handleCanvasClickCore(ev.clientX, ev.clientY, ev.button > 0);
- }
-
- protected canvasTouchHandler = (ev: React.TouchEvent) => {
- this.handleCanvasClickCore(ev.changedTouches[0].clientX, ev.changedTouches[0].clientY, false);
- }
-
- protected handleCanvasClickCore(clientX: number, clientY: number, isRightClick: boolean) {
- const bounds = this.canvas.getBoundingClientRect();
- const column = ((clientX - bounds.left) / (bounds.width / 4)) | 0;
- const row = ((clientY - bounds.top) / (bounds.height / 4)) | 0;
+ protected handleTileClick(buttonIndex: number, isRightClick: boolean) {
+ const tile = this.renderedTiles[buttonIndex];
- const tile = this.renderedTiles[row * 4 + column];
if (tile) {
let index: number;
let qname: string;
@@ -456,19 +426,6 @@ class TilePaletteImpl extends React.Component {
}
}
- protected positionCreateTileButton() {
- const button = this.refs["create-tile-ref"] as HTMLDivElement;
-
- if (button) {
- const column = this.categoryTiles.length % 4;
- const row = Math.floor(this.categoryTiles.length / 4) % 4;
-
- button.style.position = "absolute";
- button.style.left = "calc(" + (column / 4) + " * (100% - 0.5rem) + 0.25rem)";
- button.style.top = "calc(" + (row / 4) + " * (100% - 0.5rem) + 0.25rem)";
- }
- }
-
protected foregroundBackgroundClickHandler = () => {
if (this.props.drawingMode != TileDrawingMode.Default) {
this.props.dispatchChangeDrawingMode(TileDrawingMode.Default);
@@ -512,36 +469,56 @@ class TilePaletteImpl extends React.Component {
}
}
+interface TileButtonProps {
+ tile: pxt.sprite.BitmapData;
+ title: string;
+ onClick: () => void;
+ onRightClick: () => void;
+ colors: string[];
+}
+
+const TileButton = (props: TileButtonProps) => {
+ const { tile, title, onClick, onRightClick, colors } = props;
+
+ const canvasRef = React.useRef();
+
+ React.useEffect(() => {
+ const canvas = canvasRef.current;
+
+ canvas.width = tile.width;
+ canvas.height = tile.height;
+
+ const context = canvas.getContext("2d");
+
+ context.clearRect(0, 0, canvas.width, canvas.height);
-function pageControls(pages: number, selected: number, onClick: (index: number) => void) {
- const width = 16 + (pages - 1) * 5;
- const pageMap: boolean[] = [];
- for (let i = 0; i < pages; i++) pageMap[i] = i === selected;
-
- return
+ }, [tile, colors])
+
+ return (
+
+ }
+ />
+
+ )
}
-
function mapStateToProps({ store: { present }, editor }: ImageEditorStore, ownProps: any) {
let state = (present as TilemapState);
if (!state) return {};
diff --git a/webapp/src/components/ImageFieldEditor.tsx b/webapp/src/components/ImageFieldEditor.tsx
index e1e0adcaa500..74f1aeb23110 100644
--- a/webapp/src/components/ImageFieldEditor.tsx
+++ b/webapp/src/components/ImageFieldEditor.tsx
@@ -8,9 +8,10 @@ import { obtainShortcutLock, releaseShortcutLock } from "./ImageEditor/keyboardS
import { GalleryTile, setTelemetryFunction } from './ImageEditor/store/imageReducer';
import { FilterPanel } from './FilterPanel';
import { fireClickOnEnter } from "../util";
-import { EditorToggle, EditorToggleItem, BasicEditorToggleItem } from "../../../react-common/components/controls/EditorToggle";
+import { EditorToggle } from "../../../react-common/components/controls/EditorToggle";
import { MusicFieldEditor } from "./MusicFieldEditor";
import { classList } from "../../../react-common/components/util";
+import { FocusTrap, FocusTrapRegion } from "../../../react-common/components/controls/FocusTrap";
export interface ImageFieldEditorProps {
singleFrame: boolean;
@@ -20,10 +21,6 @@ export interface ImageFieldEditorProps {
includeSpecialTagsInFilter?: boolean;
}
-interface ToggleOption extends BasicEditorToggleItem {
- view: string;
-}
-
export interface ImageFieldEditorState {
currentView: "editor" | "gallery" | "my-assets";
filterOpen: boolean;
@@ -36,11 +33,6 @@ export interface ImageFieldEditorState {
hideCloseButton?: boolean;
}
-interface ProjectGalleryItem extends pxt.sprite.GalleryItem {
- assetType: pxt.AssetType;
- id: string;
-}
-
export interface AssetEditorCore {
getAsset(): pxt.Asset;
getPersistentData(): any;
@@ -63,6 +55,7 @@ export class ImageFieldEditor extends React.Component extends React.Component
- {showHeader &&
-
-
- i.view === currentView)}
- />
-
-
-
-
-
+ return (
+
+ {showHeader &&
+
+
+ i.view === currentView)}
+ />
+
+
+
-
{lf("Filter")}
+ {!editingTile && !hideCloseButton &&
+
+
}
- {!editingTile && !hideCloseButton &&
-
-
}
-
- }
-
-
- {this.props.isMusicEditor ?
-
:
-
}
+
+
+
+ {this.props.isMusicEditor ?
+ :
+
+ }
+
+
- }
-
-
-
-
-
+
+ );
}
componentDidMount() {
@@ -621,23 +624,44 @@ export class ImageFieldEditor
extends React.Component {
+ if (ref) this.imageEditorRegion = ref;
+ }
+
+ protected onEscapeFromGallery = () => {
+ this.setState({
+ currentView: "editor"
+ }, () => {
+ if (this.imageEditorRegion) {
+ this.imageEditorRegion.focus();
+ }
+ })
+ }
}
interface ImageEditorGalleryProps {
items?: pxt.Asset[];
hidden: boolean;
onAssetSelected: (item: pxt.Asset) => void;
+ onEscape: () => void;
}
class ImageEditorGallery extends React.Component {
render() {
- let { items, hidden } = this.props;
-
- return
- {!hidden && items && items.map((item, index) =>
-
- )}
-
+ let { items, hidden, onEscape } = this.props;
+
+ return (
+
+ {!hidden && items && items.map((item, index) =>
+
+ )}
+
+ );
}
clickHandler = (asset: pxt.Asset) => {
diff --git a/webapp/src/components/assetEditor/assetCard.tsx b/webapp/src/components/assetEditor/assetCard.tsx
index 73a1c1c465c3..e0eb892cbd01 100644
--- a/webapp/src/components/assetEditor/assetCard.tsx
+++ b/webapp/src/components/assetEditor/assetCard.tsx
@@ -5,6 +5,7 @@ import { AssetEditorState, isGalleryAsset } from './store/assetEditorReducerStat
import { dispatchChangeSelectedAsset } from './actions/dispatch';
import { AssetPreview } from "./assetPreview";
+import { fireClickOnEnter } from "../../util";
interface AssetCardProps {
@@ -57,17 +58,25 @@ export class AssetCardView extends React.Component {
const inGallery = isGalleryAsset(asset);
const icon = this.getDisplayIconForAsset(asset.type);
const showIcons = icon || !asset.meta?.displayName;
- return
-
- {showIcons &&
- {icon &&
-
+ return (
+
+
+ {showIcons &&
+ {icon &&
+
+
}
+ {!asset.meta?.displayName && !inGallery &&
+
+
}
}
- {!asset.meta?.displayName && !inGallery &&
-
-
}
-
}
-
+
+ );
}
}