diff --git a/src/view/components/Simulator.tsx b/src/view/components/Simulator.tsx index a89f1289e..179e16a89 100644 --- a/src/view/components/Simulator.tsx +++ b/src/view/components/Simulator.tsx @@ -103,9 +103,17 @@ class Simulator extends React.Component { private handleClick(button: HTMLElement, active: boolean) { const ButtonA: boolean = button.id.match(/BTN_A/) !== null; const ButtonB: boolean = button.id.match(/BTN_B/) !== null; + const ButtonAB: boolean = button.id.match(/BTN_AB/) !== null; let innerButton; let newState; - if (ButtonA) { + if (ButtonAB) { + innerButton = window.document.getElementById("BTN_AB_INNER"); + newState = { + button_a: active, + button_b: active + }; + this.setState(newState); + } else if (ButtonA) { innerButton = window.document.getElementById("BTN_A_INNER"); newState = { button_a: active diff --git a/src/view/components/cpx/Accessibility_utils.ts b/src/view/components/cpx/Accessibility_utils.ts index bdd36df64..99becbebd 100644 --- a/src/view/components/cpx/Accessibility_utils.ts +++ b/src/view/components/cpx/Accessibility_utils.ts @@ -4,6 +4,16 @@ namespace accessibility { elem.setAttribute("focusable", "true"); elem.setAttribute("tabindex", "0"); } + + export function setAria(elem: Element, role?: string, label?: string): void { + if (role && !elem.hasAttribute("role")) { + elem.setAttribute("role", role); + } + + if (label && !elem.hasAttribute("aria-label")) { + elem.setAttribute("aria-label", label); + } + } } export default accessibility; diff --git a/src/view/components/cpx/Cpx.tsx b/src/view/components/cpx/Cpx.tsx index ad596fd5d..4f3fe3b1e 100644 --- a/src/view/components/cpx/Cpx.tsx +++ b/src/view/components/cpx/Cpx.tsx @@ -34,6 +34,39 @@ const Cpx: React.FC = props => { return CPX_SVG; }; +const makeButton = ( + g: SVGElement, + left: number, + top: number, + id: string +): { outer: SVGElement; inner: SVGElement } => { + const buttonCornerRadius = SvgStyle.BUTTON_CORNER_RADIUS; + const buttonWidth = SvgStyle.BUTTON_WIDTH; + const buttonCircleRadius = SvgStyle.BUTTON_CIRCLE_RADIUS; + const btng = svg.child(g, "g", { class: "sim-button-group" }); + svg.child(btng, "rect", { + id: id + "_OUTER", + x: left, + y: top, + rx: buttonCornerRadius, + ry: buttonCornerRadius, + width: buttonWidth, + height: buttonWidth, + fill: SvgStyle.BUTTON_OUTER + }); + + const outer = btng; + const inner = svg.child(btng, "circle", { + id: id + "_INNER", + cx: left + buttonWidth / 2, + cy: top + buttonWidth / 2, + r: buttonCircleRadius, + fill: SvgStyle.BUTTON_NEUTRAL + }); + + return { outer, inner }; +}; + const initSvgStyle = (svgElement: HTMLElement, brightness: number): void => { let style: SVGStyleElement = svg.child( svgElement, @@ -49,6 +82,9 @@ const initSvgStyle = (svgElement: HTMLElement, brightness: number): void => { {} ) as SVGDefsElement; + let g = svg.createElement("g") as SVGElement; + svgElement.appendChild(g); + let glow = svg.child(defs, "filter", { id: "filterglow", x: "-5%", @@ -99,6 +135,20 @@ const initSvgStyle = (svgElement: HTMLElement, brightness: number): void => { type: "linear", slope: brightness }); + + // BTN A+B + const outerBtn = (left: number, top: number, label: string) => { + const button = makeButton(g, left, top, "BTN_AB"); + return button; + }; + + let ab = outerBtn(165, SvgStyle.MB_HEIGHT - 15, "A+B"); + let abtext = svg.child(ab.outer, "text", { + x: SvgStyle.BUTTON_TEXT_BASELINE, + y: SvgStyle.MB_HEIGHT - 18, + class: "sim-text" + }) as SVGTextElement; + abtext.textContent = "A+B"; }; const updateNeopixels = (props: IProps): void => { @@ -170,8 +220,8 @@ const changeBrightness = (filterID: string, brightness: number): void => { }; const setupButtons = (props: IProps): void => { - const outButtons = ["A_OUTER", "B_OUTER"]; - const inButtons = ["A_INNER", "B_INNER"]; + const outButtons = ["A_OUTER", "B_OUTER", "AB_OUTER"]; + const inButtons = ["A_INNER", "B_INNER", "AB_INNER"]; outButtons.forEach(buttonName => { const button = window.document.getElementById("BTN_" + buttonName); @@ -187,9 +237,22 @@ const setupButtons = (props: IProps): void => { }); }; +const addButtonLabels = (button: HTMLElement) => { + let label = ""; + if (button.id.match(/AB/) !== null) { + label = "A+B"; + } else if (button.id.match(/A/) !== null) { + label = "A"; + } else if (button.id.match(/B/) !== null) { + label = "B"; + } + accessibility.setAria(button, "button", label); +}; + const setupButton = (button: HTMLElement, className: string, props: IProps) => { const svgButton = (button as unknown) as SVGElement; svg.addClass(svgButton, className); + addButtonLabels(button); if (className.match(/outer/) !== null) { accessibility.makeFocusable(svgButton); } diff --git a/src/view/components/cpx/Cpx_svg_style.tsx b/src/view/components/cpx/Cpx_svg_style.tsx index 531ef92e0..74b7eb219 100644 --- a/src/view/components/cpx/Cpx_svg_style.tsx +++ b/src/view/components/cpx/Cpx_svg_style.tsx @@ -11,6 +11,11 @@ export const RED_LED_ON: string = "#FF7777"; export const RED_LED_OFF: string = "#FFFFFF"; export const BUTTON_NEUTRAL: string = "#000000"; export const BUTTON_PRESSED: string = "#FFA500"; +export const BUTTON_OUTER: string = "#DCDCDC"; +export const BUTTON_CORNER_RADIUS: number = 2; +export const BUTTON_WIDTH: number = 10; +export const BUTTON_CIRCLE_RADIUS: number = 3; +export const BUTTON_TEXT_BASELINE: number = 163; // Adapted from : https://github.com/microsoft/pxt/blob/master/pxtsim/simlib.ts export function rgbToHsl(