From c4a711309917dd8a2cd22365d764b079cb63b9cb Mon Sep 17 00:00:00 2001 From: Rhiannan Berry Date: Thu, 22 Apr 2021 22:49:14 -0400 Subject: [PATCH] Adding a11y stuff and screwing around with css --- index.html | 7 +- src/components/avatar_part_radio_group.tsx | 45 +++++- src/components/color.tsx | 48 ++++--- src/components/color_radio_group.tsx | 64 +++++++-- src/components/editor.tsx | 49 ++++--- src/components/export_buttons.tsx | 2 +- src/components/material_radio_group.tsx | 35 +++-- src/components/page_radio_group.tsx | 68 ++++++--- src/components/radio.tsx | 65 ++++++++- src/main.tsx | 3 +- src/stylesheets/buttons.scss | 108 +++++++++++++-- src/stylesheets/main.scss | 152 ++++++++++++--------- 12 files changed, 474 insertions(+), 172 deletions(-) diff --git a/index.html b/index.html index 4b344a5a..b2f88f8f 100644 --- a/index.html +++ b/index.html @@ -2,8 +2,8 @@ - IEEE VR 2020 | Avatar Customizer - + Avatar Customizer +
@@ -13,5 +13,8 @@
+
+ Hello +
\ No newline at end of file diff --git a/src/components/avatar_part_radio_group.tsx b/src/components/avatar_part_radio_group.tsx index 8b0966c8..225d7239 100644 --- a/src/components/avatar_part_radio_group.tsx +++ b/src/components/avatar_part_radio_group.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, createRef, RefObject } from 'react'; import * as PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -11,26 +11,39 @@ interface AvatarPartRadioGroupProps { avatarPart: AvatarPart; iconPaths: string[]; labels: string[]; + title: string; } export default class AvatarPartRadioGroup extends Component { disabled: boolean; isRequired: boolean; props: AvatarPartRadioGroupProps; + idPrefix: string; + partsRefs: RefObject[] = []; static propTypes = { avatarPart: PropTypes.instanceOf(AvatarPart), iconPaths: PropTypes.arrayOf(PropTypes.string), labels: PropTypes.arrayOf(PropTypes.string), + title: PropTypes.string, }; constructor(props: AvatarPartRadioGroupProps) { super(props); + this.idPrefix = this.props.title.replace(/\s/g, "-").toLowerCase(); + this.isRequired = this.props.avatarPart.isRequired; this.disablePart = this.disablePart.bind(this); this.togglePart = this.togglePart.bind(this); + + this.moveFocus = this.moveFocus.bind(this); + + const refCount = this.props.labels.length + (this.isRequired ? 0 : 1); + for (let i = 0; i < refCount; i++) { + this.partsRefs.push(createRef()); + } } componentDidMount(): void { @@ -45,6 +58,12 @@ export default class AvatarPartRadioGroup extends Component { } } + moveFocus(index: number, direction: number): void { + const length = this.partsRefs.length; + const ind = (index + direction + length) % length; + this.partsRefs[ind].current.focus(); + } + disablePart(): void { this.props.avatarPart.disable(); this.forceUpdate(); @@ -56,8 +75,14 @@ export default class AvatarPartRadioGroup extends Component { } render(): JSX.Element { + const d = this.isRequired ? 0 : 1; const disableButton = this.isRequired ? null : ( - + this.moveFocus(0, dir)} + selected={this.props.avatarPart.disabled} + className="part" + label='Disable'> ); @@ -65,20 +90,26 @@ export default class AvatarPartRadioGroup extends Component { const parts = this.props.iconPaths.map((path, i) => ( this.moveFocus(d+i, dir)} className="part" onClickCallback={this.togglePart} value={i} + label={this.props.labels[i]} selected={!this.props.avatarPart.disabled && this.props.avatarPart.isSelected(i)} + icon={path} > - )); return ( -
- {disableButton} - {parts} -
+ <> +

{this.props.title}

+
+ {disableButton} + {parts} +
+ ); } } diff --git a/src/components/color.tsx b/src/components/color.tsx index 60f6b40b..5749b1ea 100644 --- a/src/components/color.tsx +++ b/src/components/color.tsx @@ -53,10 +53,8 @@ class MyColorPicker extends Component { render(): JSX.Element { const style = { - width: '100%', position: 'relative', display: 'block', - height: '60px', maxWidth: '318px', } as React.CSSProperties; @@ -70,24 +68,36 @@ class MyColorPicker extends Component { position: 'relative', } as React.CSSProperties; + const sp = ()=> ( +
+ ) + + const hp = ()=> ( +
+ ) + return ( - -
- -
-
- -
-
+
+ +
+ +
+
+ +
+
+
); } } diff --git a/src/components/color_radio_group.tsx b/src/components/color_radio_group.tsx index 32fd72e2..519a6aa7 100644 --- a/src/components/color_radio_group.tsx +++ b/src/components/color_radio_group.tsx @@ -1,18 +1,17 @@ -import React, { Component } from 'react'; +import React, { Component, createRef, RefObject } from 'react'; import * as PropTypes from 'prop-types'; import tinycolor from 'tinycolor2'; import { ColorResult } from 'react-color'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBan } from '@fortawesome/free-solid-svg-icons/faBan'; -import { faPalette } from '@fortawesome/free-solid-svg-icons/faPalette'; - import { Material } from '../models/materials/material'; import Radio from './radio'; import MyColorPicker from './color'; +import { faTint } from '@fortawesome/free-solid-svg-icons'; interface ColorRadioGroupProps { + title: string; materials: Material[]; colors: string[]; } @@ -22,8 +21,11 @@ export default class ColorRadioGroup extends Component { disabled: boolean; selectedColor: string; customColor: string; + colorRefs: RefObject[] = []; + idPrefix: string; static propTypes = { + title: PropTypes.string, materials: PropTypes.arrayOf(PropTypes.instanceOf(Material)), colors: PropTypes.arrayOf(PropTypes.string), }; @@ -32,11 +34,19 @@ export default class ColorRadioGroup extends Component { super(props); this.customColor = tinycolor.random().toHexString(); + this.idPrefix = this.props.title.replace(/\s/g, "-").toLowerCase(); + + const refCount = this.props.colors.length + 1 + (!this.props.materials[0].isRequired ? 1 : 0); + + for (let i = 0; i< refCount; i++) { + this.colorRefs.push(createRef()); + } this.disableMaterial = this.disableMaterial.bind(this); this.setColor = this.setColor.bind(this); this.setCustomColor = this.setCustomColor.bind(this); this.setToCustomColor = this.setToCustomColor.bind(this); + this.moveFocus = this.moveFocus.bind(this); } componentDidMount(): void { @@ -46,6 +56,12 @@ export default class ColorRadioGroup extends Component { this.setColor(this.props.colors[index]); } + moveFocus(index: number, direction: number): void { + const length = this.colorRefs.length; + const ind = (index + direction + length) % length; + this.colorRefs[ind].current.focus(); + } + setColor(color: string): void { this.disabled = false; this.selectedColor = color; @@ -79,8 +95,12 @@ export default class ColorRadioGroup extends Component { let isSelected = this.disabled; const disableButton = isRequired ? null : ( - - + this.moveFocus(0, dir)} + label='Disable' + faIcon={faBan}> ); @@ -91,27 +111,43 @@ export default class ColorRadioGroup extends Component { const colors = this.props.colors.map((color, i) => ( this.moveFocus(i+(isRequired?0:1), dir)} + label={color} setTitle /> )); //TODO: make setting title on custom color work const customColorButton = ( - - + this.moveFocus(this.colorRefs.length-1, dir)} + label='Custom Color' + faIcon={faTint} + setTitle> ); return ( -
- {disableButton} - {colors} - {customColorButton} - -
+ <> +

{this.props.title}

+
+
+ {disableButton} + {colors} + {customColorButton} +
+ +
+ ); } } diff --git a/src/components/editor.tsx b/src/components/editor.tsx index cea95455..759fa666 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -59,7 +59,7 @@ export default class Editor extends Component { jacketMaterial: Material; shirtMaterial: Material; skinMaterial: Material; - selectedPage: string; + selectedPage: number; logoPaths: string[]; backLogoTextures: Texture[]; frontLogoTextures: Texture[]; @@ -113,69 +113,74 @@ export default class Editor extends Component { ]); this.props.hairPart.assignNewMaterials([this.hairMaterial]); this.props.glassesPart.assignNewMaterials([this.glassesMaterial]); - this.selectedPage = 'Body'; + this.selectedPage = 0; this.changePage = this.changePage.bind(this); } - changePage(selectedPage: string): void { + changePage(selectedPage: number): void { this.selectedPage = selectedPage; this.forceUpdate(); } render(): JSX.Element { + const classes = function(index: number, selected: number) { + return `page${selected === index ? ' selected': ''}`; + } + const label = ['body', 'hair-eyes', 'top', 'glasses']; return (
-
- Body Shape +
- Skin Color - Blush Color
-
- Hair Style +
- Hair Color - Eye Color
-
- Shirt Color +
- Jacket Front Logo
-
- Glasses +
- Color
diff --git a/src/components/export_buttons.tsx b/src/components/export_buttons.tsx index 9bcf083e..cf576b01 100644 --- a/src/components/export_buttons.tsx +++ b/src/components/export_buttons.tsx @@ -85,7 +85,7 @@ export default class ExportButton extends Component { const label = this.props.texture ? 'Export Texture' : 'Export Avatar'; const func = this.props.texture ? this.exportTexture : this.exportGLB; return ( - ); diff --git a/src/components/material_radio_group.tsx b/src/components/material_radio_group.tsx index ac778376..1fd1a484 100644 --- a/src/components/material_radio_group.tsx +++ b/src/components/material_radio_group.tsx @@ -1,7 +1,6 @@ -import React, { Component } from 'react'; +import React, { Component, RefObject, createRef } from 'react'; import * as PropTypes from 'prop-types'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBan } from '@fortawesome/free-solid-svg-icons/faBan'; import { faUpload } from '@fortawesome/free-solid-svg-icons/faUpload'; @@ -22,6 +21,7 @@ export default class MaterialRadioGroup extends Component { customSelected: boolean; selected: number; //just gonna go with key here file: React.RefObject; + radioRefs: RefObject[] = []; static propTypes = { material: PropTypes.instanceOf(Material), @@ -44,6 +44,12 @@ export default class MaterialRadioGroup extends Component { this.toggleSelected = this.toggleSelected.bind(this); this.setCustom = this.setCustom.bind(this); this.triggerClick = this.triggerClick.bind(this); + this.moveFocus = this.moveFocus.bind(this); + + const refCount = this.props.textures.length + 2; + for (let i = 0; i < refCount; i++) { + this.radioRefs.push(createRef()); + } } disable(): void { @@ -54,6 +60,12 @@ export default class MaterialRadioGroup extends Component { this.forceUpdate(); } + moveFocus(index: number, direction: number): void { + const length = this.radioRefs.length; + const ind = (index + direction + length) % length; + this.radioRefs[ind].current.focus(); + } + async toggleSelected(selected: number): Promise { if (selected == this.selected) { this.disable(); @@ -86,29 +98,36 @@ export default class MaterialRadioGroup extends Component { render(): JSX.Element { const disableButton = ( - - + this.moveFocus(0, dir)} + selected={this.disabled} + className="texture" + faIcon={faBan}> ); const textures = this.props.texturePaths.map((path, i) => ( this.moveFocus(i+1, dir)} onClickCallback={this.toggleSelected} selected={!this.disabled && !this.customSelected && this.selected == i} className="texture" + icon={path} > - )); const customButton = ( this.moveFocus(textures.length+1, dir)} onClickCallback={(): void => this.file.current.click()} selected={this.customSelected} className="texture" - > - - + faIcon={faUpload} + /> ); return (
diff --git a/src/components/page_radio_group.tsx b/src/components/page_radio_group.tsx index e2d6524e..7c20efa6 100644 --- a/src/components/page_radio_group.tsx +++ b/src/components/page_radio_group.tsx @@ -1,19 +1,22 @@ -import React, { Component } from 'react'; +import React, { Component, createRef, KeyboardEvent, RefObject } from 'react'; import * as PropTypes from 'prop-types'; import Radio from './radio'; interface PageRadioGroupProps { - iconPaths: string[]; + pageLabels: string[]; pageNames: string[]; onClickCallback: Function; } export default class PageRadioGroup extends Component { props: PageRadioGroupProps; - selectedPage: string; + selectedPage: number; + focused = -1; + tabRefs: RefObject[] = []; static propTypes = { iconPaths: PropTypes.arrayOf(PropTypes.string), + pageLabels: PropTypes.arrayOf(PropTypes.string), pageNames: PropTypes.arrayOf(PropTypes.string), onClickCallback: PropTypes.func, }; @@ -21,30 +24,59 @@ export default class PageRadioGroup extends Component { constructor(props: PageRadioGroupProps) { super(props); - this.selectedPage = this.props.pageNames[0]; + this.props.pageNames.forEach((v, i) => { + this.tabRefs.push(createRef()) + }); + + this.selectedPage = 0; this.togglePage = this.togglePage.bind(this); + this.keyDown = this.keyDown.bind(this); + } + + togglePage(i: number): void { + const pageName = this.props.pageNames[i]; + this.selectedPage = i; + this.props.onClickCallback(i); + this.forceUpdate(); } - togglePage(pageName: string): void { - this.selectedPage = pageName; - this.props.onClickCallback(pageName); + moveFocus(index: number, direction: number): void { + const length = this.tabRefs.length; + const ind = (index + direction + length) % length; + this.tabRefs[ind].current.focus(); this.forceUpdate(); } + keyDown(i: number, e: KeyboardEvent): void { + const key = e.key; + switch (key) { + case 'ArrowLeft': + e.preventDefault(); + this.moveFocus(i, -1); + break; + case 'ArrowRight': + e.preventDefault(); + this.moveFocus(i, 1); + break; + } + } + render(): JSX.Element { - const buttons = this.props.iconPaths.map((path, i) => ( - ( + )); - return
{buttons}
; + return
{pages}
; } } diff --git a/src/components/radio.tsx b/src/components/radio.tsx index 1f9e5ffd..34857a98 100644 --- a/src/components/radio.tsx +++ b/src/components/radio.tsx @@ -1,5 +1,7 @@ -import React, { Component } from 'react'; +import React, { Component, createRef, KeyboardEvent, RefObject } from 'react'; import * as PropTypes from 'prop-types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; interface RadioProps { children?: JSX.Element[] | JSX.Element | null; @@ -8,26 +10,34 @@ interface RadioProps { selected: boolean; setTitle: boolean; className?: string; + icon?: string; + faIcon?: IconDefinition; value?: string | number; label?: string; + onMoveFocus?: Function; + left?: RefObject; + right?: RefObject; } export default class Radio extends Component { props: RadioProps; value: string | number; + radioRef: RefObject; static propTypes = { color: PropTypes.string, + icon: PropTypes.string, onClickCallback: PropTypes.func, selected: PropTypes.bool, setTitle: PropTypes.bool, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), label: PropTypes.string, className: PropTypes.string, + onMoveFocus: PropTypes.func, }; static defaultProps = { - color: '#b8b8b8', + color: '#ece5eb', selected: false, setTitle: false, }; @@ -35,7 +45,9 @@ export default class Radio extends Component { constructor(props: RadioProps) { super(props); + this.radioRef = createRef(); this.value = this.props.value != null ? this.props.value : this.props.color; + this.keyDown = this.keyDown.bind(this); this.onClickValue = this.onClickValue.bind(this); } @@ -44,22 +56,61 @@ export default class Radio extends Component { //only send back up if SUCCESSFUL for texture upload attempts } + keyDown(e: KeyboardEvent): void{ + switch(e.key) { + case 'Enter': + case ' ': + e.preventDefault(); + this.onClickValue(); + return; + case 'ArrowLeft': + e.preventDefault(); + this.props.onMoveFocus? this.props.onMoveFocus(-1, this) : null; + return; + case 'ArrowRight': + e.preventDefault(); + this.props.onMoveFocus? this.props.onMoveFocus(1, this) : null; + return; + } + } + + focus() { + this.radioRef.current.focus(); + this.forceUpdate(); + } + render(): JSX.Element { - const swatchStyle = { - backgroundColor: this.props.color, - color: this.props.color + const isCustomColor = this.props.className === 'custom-color'; + let swatchStyle = { + backgroundColor: this.props.faIcon ? '#ece5eb' : this.props.color, + color: isCustomColor ? this.props.color : '#381327' }; const classNames = `swatch ${this.props.className} ${this.props.selected? 'selected':''}`; + return ( -
+
{this.props.children} + + { (() => { + if (this.props.icon) { + return () + } else if (this.props.faIcon) { + return () + } + })() + }
); diff --git a/src/main.tsx b/src/main.tsx index f5aed4aa..fa1f4faa 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -46,6 +46,7 @@ function initializeScene(): SceneObjects { const controls = new OrbitControls(camera, renderer.domElement); renderer.setSize(size.width, size.height); + renderer.domElement.setAttribute('aria-label', 'preview'); controls.enablePan = false; // add renderer to dom @@ -130,8 +131,8 @@ function render(s: SceneObjects, mixer: THREE.AnimationMixer): void { } function initialize(): void { - const sceneObjects = initializeScene(); importModels().then(dso => { + const sceneObjects = initializeScene(); sceneObjects.scene.add(dso.group); setInterval(() => { diff --git a/src/stylesheets/buttons.scss b/src/stylesheets/buttons.scss index c3770f39..1e9b0b76 100644 --- a/src/stylesheets/buttons.scss +++ b/src/stylesheets/buttons.scss @@ -1,5 +1,5 @@ $swatch-size : 40px; -$part-swatch-size: 60px; +$part-swatch-size: 75px; $page-swatch-size: 40px; $texture-swatch-size: 40px; $swatch-corner-radius : 4px; @@ -8,13 +8,93 @@ $swatch-background: #e0e0e0; .swatchContainer { display: flex; flex-wrap: wrap; - //justify-content: center; - & > * { - margin-bottom: 6px; + + margin-top: 6px; + margin-bottom: 20px; + + &.page-container { + margin-top: 0px; + margin-bottom: 0px; + + button { + background-color: transparent; + color: #381327; + cursor: pointer; + font-size: 20px; + padding: 6px 10px; + border: 0px; + margin-right: 10px; + &:hover,&:focus { + box-shadow: none; + } + &:focus-visible { + outline: dotted red; + } + + &[aria-selected='true'] { + background-color: lavenderblush; + border: pink solid 1px; + border-radius: 3px 3px 0 0; + border-bottom: none; + padding: 6px 9px; + //text-decoration: underline 2px; + } + } + } + + span:focus-visible { + outline: dotted red; + } + + &.part-container { + display: grid; + grid-gap: 6px; + grid-template-columns: repeat(auto-fill, 75px); + } + + &.color-container { + flex-direction: row; + flex-wrap: nowrap; + + .color-group { + display: grid; + grid-gap: 6px; + grid-template-columns: repeat(auto-fill, 40px); + flex-basis: 184px; + } + } + .color-picker-wrapper { + border-radius: 4px; + border: #381327 solid 2px; + height: 82px; + flex-basis: auto; + flex-grow: 1; + + .color-picker { + height:100%; + width: 100%; + .saturation-pointer { + width: 10px; + height: 10px; + border-radius: 50%; + //border: white solid 3px; + box-shadow: white 0 0 0 2px, #381327 0 0 0 4px, inset #381327 0 0 0 1px; + transform: translate(-5px, -5px); + cursor: pointer; + } + + .hue-pointer { + width: 4px; + height: 18px; + //margin-top: -1px; + box-shadow: white 0 0 0 2px, #381327 0 0 0 4px, inset #381327 0 0 0 1px; + border-radius: 3px; + transform: translateX(-2px); + cursor: pointer; + } + + } } - margin-top: 3px; - margin-bottom: 6px; - max-width: 350px; } .swatch { @@ -27,7 +107,7 @@ $swatch-background: #e0e0e0; &.selected { .inner { - box-shadow: 0 0 3px 2px; + box-shadow: #ff66ae inset 0 0 0 2px, #502f41 inset 0 0 0 4px; } } @@ -54,6 +134,12 @@ $swatch-background: #e0e0e0; width: 100%; height:100%; border-radius: $swatch-corner-radius; + color: #381327; + box-shadow: #502f41 inset 0 0 0 2px; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + pointer-events: none; &.custom { background-color: $swatch-background !important; @@ -103,7 +189,11 @@ $swatch-background: #e0e0e0; height: 80%; object-fit: contain; -o-object-fit: contain; - color: black; + //color: #381327; + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + pointer-events: none; } } diff --git a/src/stylesheets/main.scss b/src/stylesheets/main.scss index b07017b7..dad56df0 100644 --- a/src/stylesheets/main.scss +++ b/src/stylesheets/main.scss @@ -2,12 +2,24 @@ body { font-family: Arial, Helvetica, sans-serif; font-weight: bold; display: flex; - justify-content: center; + margin: 0px; + flex-direction: column; + align-items: center; + + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; } #container { margin-top: 30px; - max-width: 900px; + margin-bottom: 30px; + min-height: calc(100vh - 90px); + max-width: 860px; + justify-content: space-between; display: flex; flex-direction: row; width: 100%; @@ -15,32 +27,92 @@ body { #left { display: flex; flex-direction: column; + align-items: center; + margin-bottom: 20px; + + canvas { + width: 400px; + height: 500px; + } #buttons { + margin-top: 8px; display: grid; grid-column-gap: 6px; grid-template-columns: auto auto; - padding: 4px 0; } } - canvas { - width: 400px; - height: 500px; - } - - @media only screen and (max-width: 900px) { + #options { + display: inline-flex; flex-direction: column; - canvas { - margin-bottom: 20px; + vertical-align: top; + height: 100%; + width: 442px; + min-width: 320px; + + color: #753e5c; + + .page { + padding: 20px; + background-color: lavenderblush; + border: solid 1px pink; + border-radius: 0 6px 6px 0; + margin-top: -1px; + + display: none; + &.selected { + display: block; + } } - } - @media only screen and (min-device-width : 320px) and (max-device-width : 480px) { - + .texture-layer { + height: 32px; + margin: 10px; + + .layer-button-group { + margin: 0px 5px; + .layer-button { + display:block; + color: #8d8d8d; + + :hover { + color:#010101; + -webkit-transition-duration: 0.2s; /* Safari */ + transition-duration: 0.2s; + -moz-transition-duration: 0.2s; + } + + >svg { + display: block; + } + + } + } + + .color-picker { + height:100%; + width:32px; + padding: 0px; + //border: 0px; + margin: 0px 10px; + } + + .dropdown { + height:100%; + width: 100px; + } + } + } + + + @media only screen and (max-width: 900px) { flex-direction: column; - canvas { - margin-bottom: 20px; + align-items: center; + #options { + width: 100%; + max-width: 400px; + padding: 0; } } } @@ -53,52 +125,4 @@ td { & > div { display:contents; } -} - -#options { - display: inline-flex; - flex-direction: column; - vertical-align: top; - height: 100%; - width: 100%; - padding: 0 20px; - min-width: 320px; - - .texture-layer { - height: 32px; - margin: 10px; - - .layer-button-group { - margin: 0px 5px; - .layer-button { - display:block; - color: #8d8d8d; - - :hover { - color:#010101; - -webkit-transition-duration: 0.2s; /* Safari */ - transition-duration: 0.2s; - -moz-transition-duration: 0.2s; - } - - >svg { - display: block; - } - - } - } - - .color-picker { - height:100%; - width:32px; - padding: 0px; - //border: 0px; - margin: 0px 10px; - } - - .dropdown { - height:100%; - width: 100px; - } - } } \ No newline at end of file