diff --git a/example/index.html b/example/index.html index 90c9ff1..0ad96ae 100644 --- a/example/index.html +++ b/example/index.html @@ -8,6 +8,57 @@ </head> <body> + <header> + <button id="mainimg" aria-controls="mainnav" aria-expanded="false" role="menuitem" tabindex="0"> + PICK YOUR STARTER + </button> + <h1>STARTER SELECTION</h1> + <nav> + <ul role="menu"> + <li role="none"> + <button aria-controls="mainnav" aria-expanded="false" role="menuitem" tabindex="0"> + PICK YOUR STARTER + </button> + </li> + <ul id="mainnav"> + <li role="none"> + <a href="/">GRASS TYPE</a> + </li> + <li> + <a href="/red">FIRE TYPE</a> + </li> + <li> + <a href="/blue">WATER TYPE</a> + </li> + </ul> + </ul> + </nav> + </header> + <form> + <fieldset> + <legend> + <p>Select option</p> + </legend> + <label> + Large + <input tabindex="0" type="radio" name="sizeoption" value="large" checked="" /> + </label> + <label> + Medium + <input tabindex="0" type="radio" name="sizeoption" value="medium" /> + </label> + <label> + Small + <input tabindex="0" type="radio" name="sizeoption" value="small" /> + </label> + </fieldset> + </form> + <div> + <button tabindex="0">rotate left</button> + </div> + <div> + <button tabindex="0">rotate right</button> + </div> <div id="root"></div> <script src="./src/index.js"></script> </body> diff --git a/example/public/index.html b/example/public/index.html index 42ae2d2..16b054f 100644 --- a/example/public/index.html +++ b/example/public/index.html @@ -1,17 +1,16 @@ <!DOCTYPE html> <html lang="en"> - -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> - <meta name="theme-color" content="#000000"> - <!-- + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> + <meta name="theme-color" content="#000000" /> + <!-- manifest.json provides metadata used when your web app is added to the homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ --> - <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> - <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> - <!-- + <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> + <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" /> + <!-- Notice the use of %PUBLIC_URL% in the tags above. It will be replaced with the URL of the `public` folder during the build. Only files inside the `public` folder can be referenced from the HTML. @@ -20,15 +19,15 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <title>React App</title> -</head> + <title>React App</title> + </head> -<body> - <noscript> - You need to enable JavaScript to run this app. - </noscript> - <div id="root"></div> - <!-- + <body> + <noscript> + You need to enable JavaScript to run this app. + </noscript> + <div id="root"></div> + <!-- This HTML file is a template. If you open it directly in the browser, you will see an empty page. @@ -38,6 +37,5 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> -</body> - -</html> \ No newline at end of file + </body> +</html> diff --git a/example/src/App.js b/example/src/App.js index b6dce62..e029473 100644 --- a/example/src/App.js +++ b/example/src/App.js @@ -1,8 +1,8 @@ import * as THREE from "three" import { Canvas, useFrame, useThree } from "@react-three/fiber" -import React, { Suspense, useCallback, useEffect, useRef, useContext } from "react" +import React, { Suspense, useCallback, useEffect, useRef, useContext, useState } from "react" import { ContactShadows, Text, Html } from "@react-three/drei" -import { A11y, useA11y, A11yAnnouncer, A11yUserPreferences, useUserPreferences, A11ySection, A11yDebuger } from "../../" +import { A11y, useA11y, A11yAnnouncer, A11yUserPreferences, useUserPreferences } from "../../" import { ResizeObserver } from "@juggle/resize-observer" import { proxy, useProxy } from "valtio" import { EffectComposer, SSAO, SMAA } from "@react-three/postprocessing" @@ -17,21 +17,21 @@ const geometries = [ new THREE.IcosahedronBufferGeometry(1.5), ] -function ToggleButton(props) { - const a11y = useA11y() - return ( - <mesh {...props}> - <torusGeometry args={[0.5, a11y.pressed ? 0.28 : 0.25, 16, 32]} /> - <meshStandardMaterial color={a11y.focus ? "lightsalmon" : a11y.hover ? "lightpink" : "lightblue"} /> - </mesh> - ) -} +// function ToggleButton(props) { +// const a11y = useA11y() +// return ( +// <mesh {...props}> +// <torusGeometry args={[0.5, a11y.pressed ? 0.28 : 0.25, 16, 32]} /> +// <meshStandardMaterial color={a11y.focus ? "lightsalmon" : a11y.hover ? "lightpink" : "lightblue"} /> +// </mesh> +// ) +// } function SwitchButton(props) { const a11y = useA11y() return ( <> - <mesh {...props} rotation={[0, 0, a11y.pressed ? Math.PI / 4 : -Math.PI / 4]}> + <mesh {...props} rotation={[0, 0, props.checked ? Math.PI / 4 : -Math.PI / 4]}> <boxBufferGeometry args={[0.3, 2, 0.3]} /> <meshStandardMaterial color={a11y.focus ? "lightsalmon" : a11y.hover ? "lightpink" : "lightblue"} /> </mesh> @@ -43,74 +43,80 @@ function SwitchButton(props) { ) } -function Floor(props) { - return ( - <> - <ContactShadows rotation-x={Math.PI / 2} position={[0, -5, 0]} opacity={0.4} width={30} height={30} blur={1} far={15} /> - <mesh {...props} position={[0, -5.1, 0]} rotation={[-Math.PI / 2, 0, 0]}> - <planeBufferGeometry args={[30, 30, 1]} /> - <meshStandardMaterial color={"#eef5f7"} /> - </mesh> - </> - ) -} +// function Floor(props) { +// return ( +// <> +// <ContactShadows rotation-x={Math.PI / 2} position={[0, -5, 0]} opacity={0.4} width={30} height={30} blur={1} far={15} /> +// <mesh {...props} position={[0, -5.1, 0]} rotation={[-Math.PI / 2, 0, 0]}> +// <planeBufferGeometry args={[30, 30, 1]} /> +// <meshStandardMaterial color={"#eef5f7"} /> +// </mesh> +// </> +// ) +// } -function Nav({ left }) { - const snap = useProxy(state) - const viewport = useThree(state => state.viewport) - const radius = Math.min(12, viewport.width / 2.5) - return ( - <A11y - role="button" - description={`Spin ${left ? "left" : "right"} shape`} - activationMsg="shape showing" - actionCall={() => { - state.rotation = snap.rotation + ((Math.PI * 2) / 5) * (left ? -1 : 1) - state.active = left ? (snap.active === 0 ? 4 : snap.active - 1) : snap.active === 4 ? 0 : snap.active + 1 - }} - disabled={snap.disabled}> - <Diamond position={[left ? -radius : radius, 0, 0]} rotation={[0, 0, -Math.PI / 4]} /> - </A11y> - ) -} +// function Nav({ left }) { +// const snap = useProxy(state) +// const viewport = useThree(state => state.viewport) +// const radius = Math.min(12, viewport.width / 2.5) +// return ( +// <A11yTag tag="li" a11yElAttr={{ role: "treeitem", "aria-expanded": "false" }}> +// <A11y +// role="button" +// href="#" +// description={`Spin ${left ? "left" : "right"} shape`} +// a11yElAttr={state.active === 4 ? { role: "treeitem", "aria-expanded": "false" } : {}} +// parentElAttr={{ role: "treeitem", "aria-expanded": "false" }} +// activationMsg="shape showing" +// parentTag="li" +// actionCall={() => { +// state.rotation = snap.rotation + ((Math.PI * 2) / 5) * (left ? -1 : 1) +// state.active = left ? (snap.active === 0 ? 4 : snap.active - 1) : snap.active === 4 ? 0 : snap.active + 1 +// }} +// disabled={snap.disabled}> +// <Diamond position={[left ? -radius : radius, 0, 0]} rotation={[0, 0, -Math.PI / 4]} /> +// </A11y> +// </A11yTag> +// ) +// } -function Diamond({ position, rotation }) { - const a11y = useA11y() - return ( - <mesh position={position} rotation={rotation}> - <tetrahedronBufferGeometry /> - <meshPhongMaterial color={a11y.focus ? "lightsalmon" : a11y.hover ? "lightpink" : "lightblue"} /> - </mesh> - ) -} +// function Diamond({ position, rotation }) { +// const a11y = useA11y() +// return ( +// <mesh position={position} rotation={rotation}> +// <tetrahedronBufferGeometry /> +// <meshPhongMaterial color={a11y.focus ? "lightsalmon" : a11y.hover ? "lightpink" : "lightblue"} /> +// </mesh> +// ) +// } -function Shape({ index, active, ...props }) { - const snap = useProxy(state) - const vec = new THREE.Vector3() - const ref = useRef() - const { a11yPrefersState } = useUserPreferences() - useFrame((state, delta) => { - if (snap.disabled) { - return - } - if (a11yPrefersState.prefersReducedMotion) { - const s = active ? 2 : 1 - ref.current.scale.set(s, s, s) - ref.current.rotation.y = ref.current.rotation.x = active ? 1.5 : 4 - ref.current.position.y = 0 - } else { - const s = active ? 2 : 1 - ref.current.scale.lerp(vec.set(s, s, s), 0.1) - ref.current.rotation.y = ref.current.rotation.x += delta / (active ? 1.5 : 4) - ref.current.position.y = active ? Math.sin(state.clock.elapsedTime) / 2 : 0 - } - }) - return ( - <mesh rotation-y={index * 2000} ref={ref} {...props} geometry={geometries[index]}> - <meshPhongMaterial color={a11yPrefersState.prefersDarkScheme ? "#000000" : "#ffffff"} /> - </mesh> - ) -} +// function Shape({ index, active, ...props }) { +// const snap = useProxy(state) +// const vec = new THREE.Vector3() +// const ref = useRef() +// const { a11yPrefersState } = useUserPreferences() +// useFrame((state, delta) => { +// if (snap.disabled) { +// return +// } +// if (a11yPrefersState.prefersReducedMotion) { +// const s = active ? 2 : 1 +// ref.current.scale.set(s, s, s) +// ref.current.rotation.y = ref.current.rotation.x = active ? 1.5 : 4 +// ref.current.position.y = 0 +// } else { +// const s = active ? 2 : 1 +// ref.current.scale.lerp(vec.set(s, s, s), 0.1) +// ref.current.rotation.y = ref.current.rotation.x += delta / (active ? 1.5 : 4) +// ref.current.position.y = active ? Math.sin(state.clock.elapsedTime) / 2 : 0 +// } +// }) +// return ( +// <mesh rotation-y={index * 2000} ref={ref} {...props} geometry={geometries[index]}> +// <meshPhongMaterial color={a11yPrefersState.prefersDarkScheme ? "#000000" : "#ffffff"} /> +// </mesh> +// ) +// } // const ResponsiveText = () => { // const { viewport } = useThree() @@ -144,63 +150,61 @@ function Shape({ index, active, ...props }) { // ) // } -function Carroussel() { - const viewport = useThree(state => state.viewport) - const snap = useProxy(state) - const group = useRef() - const radius = Math.min(6, viewport.width / 5) - const { a11yPrefersState } = useUserPreferences() - useFrame(() => { - if (a11yPrefersState.prefersReducedMotion) { - group.current.rotation.y = snap.rotation - Math.PI / 2 - } else { - group.current.rotation.y = THREE.MathUtils.lerp(group.current.rotation.y, snap.rotation - Math.PI / 2, 0.1) - } - }) - return ( - <group ref={group}> - {["sphere", "pyramid", "donut", "octahedron", "icosahedron"].map((name, i) => ( - <A11y key={name} role="content" description={`a ${name}`} tabIndex={-1} hidden={snap.active !== i}> - <Shape - index={i} - position={[radius * Math.cos(i * ((Math.PI * 2) / 5)), 0, radius * Math.sin(i * ((Math.PI * 2) / 5))]} - active={snap.active === i} - color={name} - /> - </A11y> - ))} - </group> - ) -} +// function Carroussel() { +// const viewport = useThree(state => state.viewport) +// const snap = useProxy(state) +// const group = useRef() +// const radius = Math.min(6, viewport.width / 5) +// const { a11yPrefersState } = useUserPreferences() +// useFrame(() => { +// if (a11yPrefersState.prefersReducedMotion) { +// group.current.rotation.y = snap.rotation - Math.PI / 2 +// } else { +// group.current.rotation.y = THREE.MathUtils.lerp(group.current.rotation.y, snap.rotation - Math.PI / 2, 0.1) +// } +// }) +// return ( +// <group ref={group}> +// {["sphere", "pyramid", "donut", "octahedron", "icosahedron"].map((name, i) => ( +// <A11y key={name} role="content" tag="p" description={`a ${name}`} tabIndex={-1} hidden={snap.active !== i}> +// <Shape +// index={i} +// position={[radius * Math.cos(i * ((Math.PI * 2) / 5)), 0, radius * Math.sin(i * ((Math.PI * 2) / 5))]} +// active={snap.active === i} +// color={name} +// /> +// </A11y> +// ))} +// </group> +// ) +// } -const CarrousselAll = () => { - const snap = useProxy(state) +// const CarrousselAll = () => { +// const snap = useProxy(state) - return ( - <> - <A11ySection - label="Shape carousel" - description="This carousel contains 5 shapes. Use the Previous and Next buttons to cycle through all the shapes."> - <Nav left /> - <Carroussel /> - <Nav /> - <Floor /> - <A11y - role="togglebutton" - description="Light lowering button" - pressedDescription="Light lowering button, activated" - actionCall={() => (state.dark = !snap.dark)} - activationMsg="Lower light enabled" - deactivationMsg="Lower light disabled" - disabled={snap.disabled} - debug={true} - a11yElStyle={{ marginLeft: "-40px" }}> - <ToggleButton position={[0, -3, 9]} /> - </A11y> - </A11ySection> - </> - ) -} +// return ( +// <> +// <A11yTag tag={"section"} a11yElAttr={{ "aria-label": "Shape carousel" }}> +// <Nav left /> +// <Carroussel /> +// <Nav /> +// <Floor /> +// <A11y +// role="togglebutton" +// description="Light lowering button" +// pressedDescription="Light lowering button, activated" +// actionCall={() => (state.dark = !snap.dark)} +// activationMsg="Lower light enabled" +// deactivationMsg="Lower light disabled" +// disabled={snap.disabled} +// debug={true} +// a11yElStyle={{ marginLeft: "-40px" }}> +// <ToggleButton position={[0, -3, 9]} /> +// </A11y> +// </A11yTag> +// </> +// ) +// } export default function App() { // const sectionRef = useCallback(node => { @@ -208,42 +212,44 @@ export default function App() { // sectionRefref.current = node // }, []) const snap = useProxy(state) + const [checkedSize, setcheckedSize] = useState(false) return ( - <main className={snap.dark ? "dark" : "bright"}> - <Canvas resize={{ polyfill: ResizeObserver }} camera={{ position: [0, 0, 15], near: 4, far: 30 }} pixelRatio={[1, 1.5]}> - <A11yUserPreferences debug={true}> - <A11yDebuger /> - {/* <ResponsiveText /> */} - <pointLight position={[100, 100, 100]} intensity={snap.disabled ? 0.2 : 0.5} /> - <pointLight position={[-100, -100, -100]} intensity={1.5} color="red" /> - <ambientLight intensity={snap.disabled ? 0.2 : 0.8} /> - <group position-y={2}> - <CarrousselAll /> - <A11y role="image" description="Je suis un test"> - <SwitchButton position={[-3, 3, 7]} /> - </A11y> - <A11y - role="togglebutton" - startPressed={false} - description="Power button, click to disable the scene" - pressedDescription="Power button, click to turn on the scene" - actionCall={() => (state.disabled = !snap.disabled)} - activationMsg="Scene activated" - deactivationMsg="Scene disabled"> - <SwitchButton position={[-3, -5, 7]} /> - </A11y> - </group> - {/* <Suspense fallback={null}> - <EffectComposer multisampling={0}> - <SSAO radius={20} intensity={50} luminanceInfluence={0.1} color="#154073" /> - <SMAA /> - </EffectComposer> - </Suspense> */} - </A11yUserPreferences> - </Canvas> - <Badge /> - <A11yAnnouncer /> - </main> + <> + <h2 id="h2test">test</h2> + <main className={snap.dark ? "dark" : "bright"}> + <Canvas resize={{ polyfill: ResizeObserver }} camera={{ position: [0, 0, 15], near: 4, far: 30 }} pixelRatio={[1, 1.5]}> + <A11y bind="h1id"> + <SwitchButton position={[-3, 0, 7]} /> + </A11y> + <A11y bind="h2test"> + <SwitchButton position={[-3, -3, 7]} /> + </A11y> + <A11y bind="h3test"> + <SwitchButton position={[0, 0, 7]} /> + </A11y> + <A11yUserPreferences debug={true}> + {/* <A11yDebuger /> */} + {/* <ResponsiveText /> */} + <pointLight position={[100, 100, 100]} intensity={snap.disabled ? 0.2 : 0.5} /> + <pointLight position={[-100, -100, -100]} intensity={1.5} color="red" /> + <ambientLight intensity={snap.disabled ? 0.2 : 0.8} /> + <group position-y={2}> + <A11y + bind="mainimg" + actionCall={() => { + console.log(checkedSize) + setcheckedSize(!checkedSize) + }}> + <SwitchButton position={[-3, 3, 7]} /> + </A11y> + </group> + </A11yUserPreferences> + </Canvas> + <Badge /> + <A11yAnnouncer /> + </main> + <h3 id="h3test">test</h3> + </> ) } diff --git a/example/src/index.js b/example/src/index.js index 0c418c0..59700a8 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -4,4 +4,10 @@ import "./styles.css" import App from "./App" const rootElement = document.getElementById("root") -ReactDOM.render(<App />, rootElement) +ReactDOM.render( + <> + <h1 id="h1id">test h1 render</h1> + <App /> + </>, + rootElement, +) diff --git a/example/src/index.tsx b/example/src/index.tsx index f96f04f..38e9003 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -2,7 +2,7 @@ import * as THREE from "three" import ReactDOM from "react-dom" import React, { useRef, useState } from "react" import { Canvas, useFrame } from "@react-three/fiber" -import { A11y, useA11y, A11yAnnouncer, A11yUserPreferences, useUserPreferences, A11ySection, A11yDebuger } from "../../" +import { A11y } from "../../" /* just to test tsx autocomplete etc */ function Box(props: JSX.IntrinsicElements["mesh"]) { @@ -27,11 +27,14 @@ function Box(props: JSX.IntrinsicElements["mesh"]) { } ReactDOM.render( - <Canvas> - <ambientLight /> - <pointLight position={[10, 10, 10]} /> - <Box position={[-1.2, 0, 0]} /> - <Box position={[1.2, 0, 0]} /> - </Canvas>, + <> + <h1 id="h1id">test h1 render</h1> + <Canvas> + <ambientLight /> + <pointLight position={[10, 10, 10]} /> + <Box position={[-1.2, 0, 0]} /> + <Box position={[1.2, 0, 0]} /> + </Canvas> + </>, document.getElementById("root"), ) diff --git a/example/src/styles.css b/example/src/styles.css index 81e6c9a..990a1f7 100644 --- a/example/src/styles.css +++ b/example/src/styles.css @@ -21,7 +21,7 @@ main.dark { background: lightgrey; } -a { +main > a { cursor: pointer; position: absolute; bottom: 25px; diff --git a/package.json b/package.json index a8a1369..81f5659 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@react-three/a11y", - "version": "2.2.0-alpha.0", + "version": "2.2.1", "description": "👩🦯 Provide accessibility support to R3F such as focus indication, keyboard tab index, and screen reader support", "keywords": [ "a11y", @@ -72,6 +72,7 @@ ], "devDependencies": { "@babel/core": "^7.13.10", + "@react-three/fiber": "^6.0.1", "@size-limit/preset-small-lib": "^4.10.1", "@storybook/addon-essentials": "^6.1.21", "@storybook/addon-info": "^5.3.21", @@ -81,9 +82,9 @@ "@types/react": "^17.0.3", "@types/react-dom": "^17.0.2", "babel-loader": "^8.2.2", + "fast-deep-equal": "^3.1.3", "husky": "^5.1.3", "react": "^17.0.1", - "@react-three/fiber": "^6.0.1", "react-dom": "^17.0.1", "react-is": "^17.0.1", "size-limit": "^4.10.1", diff --git a/src/A11y.tsx b/src/A11y.tsx index bd2846b..f2c9a4e 100644 --- a/src/A11y.tsx +++ b/src/A11y.tsx @@ -1,80 +1,27 @@ import React, { useEffect, useRef, useState, useContext } from 'react'; -import { useThree } from '@react-three/fiber'; import useAnnounceStore from './announceStore'; -import { useA11ySectionContext } from './A11ySection'; import { stylesHiddenButScreenreadable } from './A11yConsts'; import { Html } from './Html'; +import isDeepEqual from 'fast-deep-equal/react'; -interface A11yCommonProps { - role: 'button' | 'togglebutton' | 'link' | 'content' | 'image'; +interface Props { children: React.ReactNode; - description: string; - tabIndex?: number; - showAltText?: boolean; + bind: string; + textContent: string; focusCall?: (...args: any[]) => any; debug?: boolean; a11yElStyle?: Object; + a11yElAttr?: Object; hidden?: boolean; + activationMsg?: string; + actionCall?: () => any; + disabled?: boolean; + showPointer?: boolean; } -type RoleProps = - | { - role: 'content'; - activationMsg?: never; - deactivationMsg?: never; - actionCall?: never; - href?: never; - disabled?: never; - startPressed?: never; - tag?: 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; - } - | { - role: 'button'; - activationMsg?: string; - deactivationMsg?: never; - actionCall?: () => any; - href?: never; - disabled?: boolean; - startPressed?: never; - tag?: never; - } - | { - role: 'togglebutton'; - activationMsg?: string; - deactivationMsg?: string; - actionCall?: () => any; - href?: never; - disabled?: boolean; - startPressed?: boolean; - tag?: never; - } - | { - role: 'link'; - activationMsg?: never; - deactivationMsg?: never; - actionCall: () => any; - href: string; - disabled?: never; - startPressed?: never; - tag?: never; - } - | { - role: 'image'; - activationMsg?: never; - deactivationMsg?: never; - actionCall?: never; - href?: never; - disabled?: never; - startPressed?: never; - tag?: never; - }; - -type Props = A11yCommonProps & RoleProps; - const A11yContext = React.createContext({ focus: false, hover: false, - pressed: false, }); A11yContext.displayName = 'A11yContext'; @@ -87,34 +34,36 @@ export { useA11y }; export const A11y: React.FC<Props> = ({ children, - description, + bind, + textContent, activationMsg, - deactivationMsg, - tabIndex, - href, - role, - showAltText = false, actionCall, focusCall, disabled, debug = false, a11yElStyle, - startPressed = false, - tag = 'p', + a11yElAttr, hidden = false, + showPointer = false, ...props }) => { + const bindedEl = useRef<HTMLElement | null>(null); + let constHiddenButScreenreadable = Object.assign( {}, stylesHiddenButScreenreadable, - { opacity: debug ? 1 : 0 }, + { opacity: debug ? 1 : 0, position: 'fixed', top: 0, left: 0 }, a11yElStyle ); + const a11yElAttrRef = useRef(a11yElAttr); + if (!isDeepEqual(a11yElAttrRef.current, a11yElAttr)) { + a11yElAttrRef.current = a11yElAttr; + } + const [a11yState, setA11yState] = useState({ hovered: false, focused: false, - pressed: startPressed ? startPressed : false, }); const a11yScreenReader = useAnnounceStore(state => state.a11yScreenReader); @@ -122,18 +71,19 @@ export const A11y: React.FC<Props> = ({ const overHtml = useRef(false); const overMesh = useRef(false); - const domElement = useThree(state => state.gl.domElement); + const documentElement = document.documentElement; - // temporary fix to prevent error -> keep track of our component's mounted state const componentIsMounted = useRef(true); useEffect(() => { + bindedEl.current = document.getElementById(bind); + editEl(); return () => { - domElement.style.cursor = 'default'; + bindedEl.current = null; + documentElement.style.cursor = 'default'; componentIsMounted.current = false; }; - }, []); // Using an empty dependency array ensures this on + }, [bind]); - React.Children.only(children); // @ts-ignore const handleOnPointerOver = e => { if (e.eventObject) { @@ -142,13 +92,16 @@ export const A11y: React.FC<Props> = ({ overHtml.current = true; } if (overHtml.current || overMesh.current) { - if (role !== 'content' && role !== 'image' && !disabled) { - domElement.style.cursor = 'pointer'; + if ( + bindedEl.current?.tagName === 'A' || + bindedEl.current?.tagName === 'BUTTON' || + showPointer + ) { + documentElement.style.cursor = 'pointer'; } setA11yState({ hovered: true, focused: a11yState.focused, - pressed: a11yState.pressed, }); } }; @@ -161,299 +114,85 @@ export const A11y: React.FC<Props> = ({ } if (!overHtml.current && !overMesh.current) { if (componentIsMounted.current) { - domElement.style.cursor = 'default'; + documentElement.style.cursor = 'default'; setA11yState({ hovered: false, focused: a11yState.focused, - pressed: a11yState.pressed, }); } } }; - function handleBtnClick() { - //msg is the same need to be clean for it to trigger again in case of multiple press in a row - a11yScreenReader(''); - window.setTimeout(() => { - if (typeof activationMsg === 'string') a11yScreenReader(activationMsg); - }, 100); - if (typeof actionCall === 'function') actionCall(); - } - - function handleToggleBtnClick() { - if (a11yState.pressed) { - if (typeof deactivationMsg === 'string') - a11yScreenReader(deactivationMsg); - } else { - if (typeof activationMsg === 'string') a11yScreenReader(activationMsg); - } - setA11yState({ - hovered: a11yState.hovered, - focused: a11yState.focused, - pressed: !a11yState.pressed, - }); - if (typeof actionCall === 'function') actionCall(); - } - - const returnHtmlA11yEl = () => { - if (role === 'button' || role === 'togglebutton') { - let disabledBtnAttr = disabled - ? { - disabled: true, - } - : null; - if (role === 'togglebutton') { - return ( - <button - r3f-a11y="true" - {...disabledBtnAttr} - aria-pressed={a11yState.pressed ? 'true' : 'false'} - tabIndex={tabIndex ? tabIndex : 0} - style={Object.assign( - constHiddenButScreenreadable, - disabled ? { cursor: 'default' } : { cursor: 'pointer' }, - hidden - ? { visibility: 'hidden' as const } - : { visibility: 'visible' as const } - )} - onPointerOver={handleOnPointerOver} - onPointerOut={handleOnPointerOut} - onClick={e => { - e.stopPropagation(); - if (disabled) { - return; - } - handleToggleBtnClick(); - }} - onFocus={() => { - if (typeof focusCall === 'function') focusCall(); - setA11yState({ - hovered: a11yState.hovered, - focused: true, - pressed: a11yState.pressed, - }); - }} - onBlur={() => { - setA11yState({ - hovered: a11yState.hovered, - focused: false, - pressed: a11yState.pressed, - }); - }} - > - {description} - </button> - ); - } else { - //regular btn - return ( - <button - r3f-a11y="true" - {...disabledBtnAttr} - tabIndex={tabIndex ? tabIndex : 0} - style={Object.assign( - constHiddenButScreenreadable, - disabled ? { cursor: 'default' } : { cursor: 'pointer' }, - hidden - ? { visibility: 'hidden' as const } - : { visibility: 'visible' as const } - )} - onPointerOver={handleOnPointerOver} - onPointerOut={handleOnPointerOut} - onClick={e => { - e.stopPropagation(); - if (disabled) { - return; - } - handleBtnClick(); - }} - onFocus={() => { - if (typeof focusCall === 'function') focusCall(); - setA11yState({ - hovered: a11yState.hovered, - focused: true, - pressed: a11yState.pressed, - }); - }} - onBlur={() => { - setA11yState({ - hovered: a11yState.hovered, - focused: false, - pressed: a11yState.pressed, - }); - }} - > - {description} - </button> + const editEl = () => { + if (bindedEl.current) { + if (bindedEl.current.tagName === 'IMG') + bindedEl.current.setAttribute( + 'src', + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E" ); + bindedEl.current.setAttribute('data-r3f-a11y', 'true'); + if (a11yElAttr) { + for (const property in a11yElAttr) { + bindedEl.current.setAttribute( + property.replace(/[A-Z]/g, m => '-' + m.toLowerCase()), + //@ts-ignore + a11yElAttr[property] + ); + } } - } else if (role === 'link') { - return ( - <a - r3f-a11y="true" - style={Object.assign( - constHiddenButScreenreadable, - hidden - ? { visibility: 'hidden' as const } - : { visibility: 'visible' as const } - )} - href={href} - onPointerOver={handleOnPointerOver} - onPointerOut={handleOnPointerOut} - onClick={e => { - e.stopPropagation(); - e.preventDefault(); - if (typeof actionCall === 'function') actionCall(); - }} - onFocus={() => { - if (typeof focusCall === 'function') focusCall(); - setA11yState({ - hovered: a11yState.hovered, - focused: true, - pressed: a11yState.pressed, - }); - }} - onBlur={() => { - setA11yState({ - hovered: a11yState.hovered, - focused: false, - pressed: a11yState.pressed, - }); - }} - > - {description} - </a> + const styles = Object.assign( + constHiddenButScreenreadable, + hidden + ? { visibility: 'hidden' as const } + : { visibility: 'visible' as const } ); - } else { - let tabIndexP = tabIndex - ? { - tabIndex: tabIndex, - } - : null; - if (role === 'image') { - return ( - <img - r3f-a11y="true" - src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E" - alt={description} - {...tabIndexP} - style={Object.assign( - constHiddenButScreenreadable, - hidden - ? { visibility: 'hidden' as const } - : { visibility: 'visible' as const } - )} - onPointerOver={handleOnPointerOver} - onPointerOut={handleOnPointerOut} - onBlur={() => { - setA11yState({ - hovered: a11yState.hovered, - focused: false, - pressed: a11yState.pressed, - }); - }} - onFocus={() => { - if (typeof focusCall === 'function') focusCall(); - setA11yState({ - hovered: a11yState.hovered, - focused: true, - pressed: a11yState.pressed, - }); - }} - /> + Object.keys(styles).forEach((key: string) => { + bindedEl.current?.style.setProperty( + key.replace(/[A-Z]/g, m => '-' + m.toLowerCase()), + //@ts-ignore + styles[key] ); - } else { - const Tag = tag; - return ( - <Tag - r3f-a11y="true" - {...tabIndexP} - style={Object.assign( - constHiddenButScreenreadable, - hidden - ? { visibility: 'hidden' as const } - : { visibility: 'visible' as const } - )} - onPointerOver={handleOnPointerOver} - onPointerOut={handleOnPointerOut} - onBlur={() => { - setA11yState({ - hovered: a11yState.hovered, - focused: false, - pressed: a11yState.pressed, - }); - }} - onFocus={() => { - if (typeof focusCall === 'function') focusCall(); - setA11yState({ - hovered: a11yState.hovered, - focused: true, - pressed: a11yState.pressed, - }); - }} - > - {description} - </Tag> - ); - } + }); + if (textContent) bindedEl.current.textContent = textContent; + bindedEl.current.onpointerover = handleOnPointerOver; + bindedEl.current.onpointerout = handleOnPointerOut; + bindedEl.current.onclick = e => { + e.stopPropagation(); + if (disabled) { + return; + } + if (typeof actionCall === 'function') actionCall(); + a11yScreenReader(''); + window.setTimeout(() => { + if (typeof activationMsg === 'string') + a11yScreenReader(activationMsg); + }, 100); + }; + bindedEl.current.onfocus = () => { + if (typeof focusCall === 'function') focusCall(); + setA11yState({ + hovered: a11yState.hovered, + focused: true, + }); + }; + bindedEl.current.onblur = () => { + setA11yState({ + hovered: a11yState.hovered, + focused: false, + }); + }; } }; - const HtmlAccessibleElement = React.useMemo(returnHtmlA11yEl, [ - description, - a11yState, - hidden, - tabIndex, - href, - disabled, - startPressed, - tag, - actionCall, - focusCall, - ]); - - let AltText = null; - if (showAltText && a11yState.hovered) { - AltText = ( - <div - aria-hidden={true} - style={{ - width: 'auto', - maxWidth: '300px', - display: 'block', - position: 'absolute', - top: '0px', - left: '0px', - transform: 'translate(-50%,-50%)', - background: 'white', - borderRadius: '4px', - padding: '4px', - }} - > - <p - aria-hidden={true} - style={{ - margin: '0px', - }} - > - {description} - </p> - </div> - ); - } + editEl(); - const section = useA11ySectionContext(); - let portal = {}; - if (section.current instanceof HTMLElement) { - portal = { portal: section }; - } + React.Children.only(children); return ( <A11yContext.Provider value={{ hover: a11yState.hovered, focus: a11yState.focused, - pressed: a11yState.pressed, }} > <group @@ -463,16 +202,11 @@ export const A11y: React.FC<Props> = ({ if (disabled) { return; } - if (role === 'button') { - handleBtnClick(); - } else if (role === 'togglebutton') { - handleToggleBtnClick(); - } else { - if (typeof actionCall === 'function') actionCall(); - } + if (typeof actionCall === 'function') actionCall(); }} onPointerOver={handleOnPointerOver} onPointerOut={handleOnPointerOut} + // visible={!hidden} > {children} <Html @@ -481,11 +215,8 @@ export const A11y: React.FC<Props> = ({ // @ts-ignore children.props.position ? children.props.position : 0 } - {...portal} - > - {AltText} - {HtmlAccessibleElement} - </Html> + target={bindedEl} + ></Html> </group> </A11yContext.Provider> ); diff --git a/src/A11yAnnouncer.tsx b/src/A11yAnnouncer.tsx index 8e49df8..5415697 100644 --- a/src/A11yAnnouncer.tsx +++ b/src/A11yAnnouncer.tsx @@ -19,7 +19,7 @@ export const A11yAnnouncer: React.FC = () => { useEffect(() => { const mouseClickListener = (e: MouseEvent) => { if ( - window.document.activeElement?.getAttribute('r3f-a11y') && + window.document.activeElement?.getAttribute('data-r3f-a11y') && e.detail !== 0 ) { if (window.document.activeElement instanceof HTMLElement) { diff --git a/src/A11yConsts.tsx b/src/A11yConsts.tsx index a0b8d8a..52c399e 100644 --- a/src/A11yConsts.tsx +++ b/src/A11yConsts.tsx @@ -4,11 +4,11 @@ let stylesHiddenButScreenreadable = { width: '50px', height: '50px', overflow: 'hidden', - transform: 'translateX(-50%) translateY(-50%)', display: 'inline-block', userSelect: 'none' as const, WebkitUserSelect: 'none' as const, WebkitTouchCallout: 'none' as const, + cursor: 'unset', margin: 0, }; diff --git a/src/A11yDebuger.tsx b/src/A11yDebuger.tsx index 77c9561..765f911 100644 --- a/src/A11yDebuger.tsx +++ b/src/A11yDebuger.tsx @@ -24,7 +24,7 @@ export const A11yDebuger: React.FC<Props> = ({}) => { const selectActiveEl = () => { console.log('focused: ', document.activeElement); let r3fa11ydebugidref = document.activeElement?.getAttribute( - 'r3f-a11y-debug-id' + 'data-r3f-a11y-debug-id' ); if (r3fa11ydebugidref) { document.querySelectorAll('[r3fa11ydebugidref]').forEach(node => { @@ -46,8 +46,8 @@ export const A11yDebuger: React.FC<Props> = ({}) => { let r3fPosId = 0; //@ts-ignore let elements = []; - document.querySelectorAll('[r3f-a11y]').forEach(node => { - node.setAttribute('r3f-a11y-debug-id', '' + r3fPosId); + document.querySelectorAll('[data-r3f-a11y]').forEach(node => { + node.setAttribute('data-r3f-a11y-debug-id', '' + r3fPosId); // let li = document.createElement('li'); // li.innerHTML = node.tagName ; // //@ts-ignore diff --git a/src/A11ySection.tsx b/src/A11ySection.tsx deleted file mode 100644 index 1c9b5dc..0000000 --- a/src/A11ySection.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { - useContext, - useEffect, - useRef, - MutableRefObject, - createRef, -} from 'react'; -import { useThree } from '@react-three/fiber'; -import { stylesHiddenButScreenreadable } from './A11yConsts'; - -interface Props { - children: React.ReactNode; - label: string; - description: string; -} - -const A11ySectionContext = React.createContext< - MutableRefObject<HTMLElement | null> ->(createRef()); - -A11ySectionContext.displayName = 'A11ySectionContext'; - -const useA11ySectionContext = () => { - return useContext(A11ySectionContext); -}; - -export { useA11ySectionContext }; - -export const A11ySection: React.FC<Props> = ({ - children, - label, - description, -}) => { - const ref = useRef<HTMLElement | null>(null); - const refpDesc = useRef<HTMLParagraphElement | null>(null); - const gl = useThree(state => state.gl); - const [el] = React.useState(() => document.createElement('section')); - const target = gl.domElement.parentNode; - - useEffect(() => { - // eslint-disable-next-line react-hooks/exhaustive-deps - if (label) { - el.setAttribute('aria-label', label); - } - el.setAttribute('r3f-a11y', 'true'); - el.setAttribute( - 'style', - (styles => { - return Object.keys(styles).reduce( - (acc, key) => - acc + - key - .split(/(?=[A-Z])/) - .join('-') - .toLowerCase() + - ':' + - (styles as any)[key] + - ';', - '' - ); - })(stylesHiddenButScreenreadable) - ); - if (description) { - if (refpDesc.current === null) { - const pDesc = document.createElement('p'); - pDesc.innerHTML = description; - pDesc.style.cssText = - 'border: 0!important;clip: rect(1px,1px,1px,1px)!important;-webkit-clip-path: inset(50%)!important;clip-path: inset(50%)!important;height: 1px!important;margin: -1px!important;overflow: hidden!important;padding: 0!important;position: absolute!important;width: 1px!important;white-space: nowrap!important;'; - el.prepend(pDesc); - refpDesc.current = pDesc; - } else { - refpDesc.current.innerHTML = description; - } - } - return () => { - if (target) target.removeChild(el); - }; - }, [description, label]); - - if (ref.current === null) { - if (target) { - target.appendChild(el); - } - ref.current = el; - } - - return ( - <> - <A11ySectionContext.Provider value={ref}> - {children} - </A11ySectionContext.Provider> - </> - ); -}; diff --git a/src/A11yUserPreferences.tsx b/src/A11yUserPreferences.tsx index 607a537..8bfd136 100644 --- a/src/A11yUserPreferences.tsx +++ b/src/A11yUserPreferences.tsx @@ -58,23 +58,31 @@ export const A11yUserPreferences: React.FC<Props> = ({ children }) => { }); }; - prefersReducedMotionMediaQuery.addEventListener( - 'change', - handleReducedMotionPrefChange - ); - prefersDarkSchemeMediaQuery.addEventListener( - 'change', - handleDarkSchemePrefChange - ); - return () => { - prefersReducedMotionMediaQuery.removeEventListener( + if (prefersReducedMotionMediaQuery) { + prefersReducedMotionMediaQuery.addEventListener( 'change', handleReducedMotionPrefChange ); - prefersDarkSchemeMediaQuery.removeEventListener( + } + if (prefersDarkSchemeMediaQuery) { + prefersDarkSchemeMediaQuery.addEventListener( 'change', handleDarkSchemePrefChange ); + } + return () => { + if (prefersReducedMotionMediaQuery) { + prefersReducedMotionMediaQuery.removeEventListener( + 'change', + handleReducedMotionPrefChange + ); + } + if (prefersDarkSchemeMediaQuery) { + prefersDarkSchemeMediaQuery.removeEventListener( + 'change', + handleDarkSchemePrefChange + ); + } }; }, []); diff --git a/src/Html.tsx b/src/Html.tsx index 501926c..cec8731 100644 --- a/src/Html.tsx +++ b/src/Html.tsx @@ -1,6 +1,5 @@ //https://raw.githubusercontent.com/pmndrs/drei/master/src/web/Html.tsx import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import { Vector3, Group, @@ -14,7 +13,6 @@ import { ReactThreeFiber, useFrame, useThree } from '@react-three/fiber'; const v1 = new Vector3(); const v2 = new Vector3(); -const v3 = new Vector3(); function calculatePosition( el: Object3D, @@ -31,14 +29,6 @@ function calculatePosition( ]; } -function isObjectBehindCamera(el: Object3D, camera: Camera) { - const objectPos = v1.setFromMatrixPosition(el.matrixWorld); - const cameraPos = v2.setFromMatrixPosition(camera.matrixWorld); - const deltaCamObj = objectPos.sub(cameraPos); - const camDir = camera.getWorldDirection(v3); - return deltaCamObj.angleTo(camDir) > Math.PI / 2; -} - function objectZIndex( el: Object3D, camera: Camera, @@ -67,94 +57,45 @@ export interface HtmlProps 'ref' > { eps?: number; - portal?: React.MutableRefObject<HTMLElement>; + target: React.MutableRefObject<HTMLElement | null>; zIndexRange?: Array<number>; } -export const Html = React.forwardRef( - ( - { - children, - eps = 0.001, - style, - className, - portal, - zIndexRange = [16777271, 0], - ...props - }: HtmlProps, - ref: React.Ref<HTMLDivElement> - ) => { - const gl = useThree(({ gl }) => gl); - const camera = useThree(({ camera }) => camera); - const scene = useThree(({ scene }) => scene); - const size = useThree(({ size }) => size); - const [el] = React.useState(() => document.createElement('div')); - const group = React.useRef<Group>(null); - const oldZoom = React.useRef(0); - const oldPosition = React.useRef([0, 0]); - const target = portal?.current ?? gl.domElement.parentNode; +export const Html = ({ + eps = 0.001, + target, + zIndexRange = [16777271, 0], + ...props +}: HtmlProps) => { + const camera = useThree(({ camera }) => camera); + const size = useThree(({ size }) => size); + const group = React.useRef<Group>(null); + const oldZoom = React.useRef(0); + const oldPosition = React.useRef([0, 0]); - React.useEffect(() => { - if (group.current) { - scene.updateMatrixWorld(); - const vec = calculatePosition(group.current, camera, size); - el.style.cssText = `position:absolute;top:0;left:0;transform:translate3d(${vec[0]}px,${vec[1]}px,0);transform-origin:0 0;`; - if (target) { - target.appendChild(el); - } - return () => { - if (target) target.removeChild(el); - ReactDOM.unmountComponentAtNode(el); - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [target]); + useFrame(() => { + if (group.current) { + camera.updateMatrixWorld(); + const vec = calculatePosition(group.current, camera, size); - const styles: React.CSSProperties = React.useMemo(() => { - return { - position: 'absolute', - transform: 'none', - ...style, - }; - }, [style, size]); - - React.useLayoutEffect(() => { - ReactDOM.render( - <div - ref={ref} - style={styles} - className={className} - children={children} - />, - el - ); - }); - - useFrame(() => { - if (group.current) { - camera.updateMatrixWorld(); - const vec = calculatePosition(group.current, camera, size); - - if ( - Math.abs(oldZoom.current - camera.zoom) > eps || - Math.abs(oldPosition.current[0] - vec[0]) > eps || - Math.abs(oldPosition.current[1] - vec[1]) > eps - ) { - el.style.display = !isObjectBehindCamera(group.current, camera) - ? 'block' - : 'none'; - el.style.zIndex = `${objectZIndex( + if ( + Math.abs(oldZoom.current - camera.zoom) > eps || + Math.abs(oldPosition.current[0] - vec[0]) > eps || + Math.abs(oldPosition.current[1] - vec[1]) > eps + ) { + if (target.current) { + target.current.style.zIndex = `${objectZIndex( group.current, camera, zIndexRange )}`; - el.style.transform = `translate3d(${vec[0]}px,${vec[1]}px,0) scale(1)`; - oldPosition.current = vec; - oldZoom.current = camera.zoom; + target.current.style.transform = `translate3d(${vec[0]}px,${vec[1]}px,0) scale(1) translate(-50%,-50%)`; } + oldPosition.current = vec; + oldZoom.current = camera.zoom; } - }); + } + }); - return <group {...props} ref={group} />; - } -); + return <group {...props} ref={group} />; +}; diff --git a/src/index.tsx b/src/index.tsx index 0e7d8e8..95c7080 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,3 @@ export * from './A11y'; export * from './A11yUserPreferences'; export * from './A11yAnnouncer'; -export * from './A11yDebuger'; -export { A11ySection } from './A11ySection';