11import "./styles.css" ;
2- import { useCallback , useRef , useState } from "react" ;
3- import { teethPaths } from "./data" ;
2+ import {
3+ type CSSProperties ,
4+ type FC ,
5+ type KeyboardEvent ,
6+ type MouseEvent ,
7+ type ReactNode ,
8+ useCallback ,
9+ useRef ,
10+ useState ,
11+ } from "react" ;
412import { OdontogramTooltip } from "./Tooltip" ;
13+ import { teethPaths } from "./data" ;
514
615type Placement =
716 | "top"
@@ -17,6 +26,23 @@ type Placement =
1726 | "left-start"
1827 | "left-end" ;
1928
29+ const placements : Record <
30+ Placement ,
31+ ( toothBox : DOMRect , margin : number ) => { x : number ; y : number }
32+ > = {
33+ top : ( t , m ) => ( { x : t . left + t . width / 2 , y : t . top - m } ) ,
34+ "top-start" : ( t , m ) => ( { x : t . left , y : t . top - m } ) ,
35+ "top-end" : ( t , m ) => ( { x : t . right , y : t . top - m } ) ,
36+ bottom : ( t , m ) => ( { x : t . left + t . width / 2 , y : t . bottom + m } ) ,
37+ "bottom-start" : ( t , m ) => ( { x : t . left , y : t . bottom + m } ) ,
38+ "bottom-end" : ( t , m ) => ( { x : t . right , y : t . bottom + m } ) ,
39+ left : ( t , m ) => ( { x : t . left - m , y : t . top + t . height / 2 } ) ,
40+ "left-start" : ( t , m ) => ( { x : t . left - m , y : t . top } ) ,
41+ "left-end" : ( t , m ) => ( { x : t . left - m , y : t . bottom } ) ,
42+ right : ( t , m ) => ( { x : t . right + m , y : t . top + t . height / 2 } ) ,
43+ "right-start" : ( t , m ) => ( { x : t . right + m , y : t . top } ) ,
44+ "right-end" : ( t , m ) => ( { x : t . right + m , y : t . bottom } ) ,
45+ } ;
2046
2147export interface TeethProps {
2248 name : string ;
@@ -25,9 +51,9 @@ export interface TeethProps {
2551 lineHighlightPath : string | string [ ] ;
2652 selected ?: boolean ;
2753 onClick ?: ( name : string ) => void ;
28- onKeyDown ?: ( e : React . KeyboardEvent < SVGGElement > , name : string ) => void ;
29- children ?: React . ReactNode ;
30- onHover ?: ( name : string , event : React . MouseEvent , placement ?: Placement ) => void ;
54+ onKeyDown ?: ( e : KeyboardEvent < SVGGElement > , name : string ) => void ;
55+ children ?: ReactNode ;
56+ onHover ?: ( name : string , event : MouseEvent , placement ?: Placement ) => void ;
3157 onLeave ?: ( ) => void ;
3258}
3359
@@ -57,8 +83,6 @@ export interface OdontogramProps {
5783 } ;
5884 showTooltip ?: boolean ;
5985 showHalf ?: "upper" | "lower" | "full" ;
60-
61-
6286}
6387
6488export function convertFDIToNotation (
@@ -151,7 +175,6 @@ export const Teeth = ({
151175 onKeyDown = { ( e ) => onKeyDown ?.( e , name ) }
152176 onMouseMove = { ( e ) => onHover ?.( name , e ) }
153177 onMouseLeave = { onLeave }
154- role = "button"
155178 aria-pressed = { selected }
156179 aria-label = { `Tooth ${ name } ` }
157180 style = { {
@@ -170,9 +193,9 @@ export const Teeth = ({
170193 />
171194 < path fill = "currentColor" d = { shadowPath } />
172195 { Array . isArray ( lineHighlightPath ) ? (
173- lineHighlightPath . map ( ( d , i ) => (
196+ lineHighlightPath . map ( ( d ) => (
174197 < path
175- key = { i }
198+ key = { ` ${ d } ` }
176199 stroke = "currentColor"
177200 strokeLinecap = "round"
178201 strokeLinejoin = "round"
@@ -190,7 +213,7 @@ export const Teeth = ({
190213 </ g >
191214) ;
192215
193- export const Odontogram : React . FC < OdontogramProps > = ( {
216+ export const Odontogram : FC < OdontogramProps > = ( {
194217 defaultSelected = [ ] ,
195218 onChange,
196219 className = "" ,
@@ -201,8 +224,7 @@ export const Odontogram: React.FC<OdontogramProps> = ({
201224 margin : 10 ,
202225 } ,
203226 showTooltip = true ,
204- showHalf = 'full' ,
205-
227+ showHalf = "full" ,
206228} ) => {
207229 const themeColors =
208230 theme === "dark"
@@ -222,7 +244,7 @@ export const Odontogram: React.FC<OdontogramProps> = ({
222244 ) ;
223245
224246 const svgRef = useRef < SVGSVGElement > ( null ) ;
225- const tooltipRef = useRef < HTMLDivElement > ( null ) ;
247+ const _tooltipRef = useRef < HTMLDivElement > ( null ) ;
226248
227249 const [ tooltipData , setTooltipData ] = useState < {
228250 active : boolean ;
@@ -270,10 +292,30 @@ export const Odontogram: React.FC<OdontogramProps> = ({
270292 label : string ;
271293 position : { x : number ; y : number } ;
272294 } > = [
273- { name : "first" , transform : "" , label : "Upper Right" , position : { x : 100 , y : 30 } } ,
274- { name : "second" , transform : "scale(-1, 1) translate(-409, 0)" , label : "Upper Left" , position : { x : 309 , y : 30 } } ,
275- { name : "third" , transform : "scale(1, -1) translate(0, -694)" , label : "Lower Right" , position : { x : 100 , y : 664 } } ,
276- { name : "fourth" , transform : "scale(-1, -1) translate(-409, -694)" , label : "Lower Left" , position : { x : 309 , y : 664 } } ,
295+ {
296+ name : "first" ,
297+ transform : "" ,
298+ label : "Upper Right" ,
299+ position : { x : 100 , y : 30 } ,
300+ } ,
301+ {
302+ name : "second" ,
303+ transform : "scale(-1, 1) translate(-409, 0)" ,
304+ label : "Upper Left" ,
305+ position : { x : 309 , y : 30 } ,
306+ } ,
307+ {
308+ name : "third" ,
309+ transform : "scale(1, -1) translate(0, -694)" ,
310+ label : "Lower Right" ,
311+ position : { x : 100 , y : 664 } ,
312+ } ,
313+ {
314+ name : "fourth" ,
315+ transform : "scale(-1, -1) translate(-409, -694)" ,
316+ label : "Lower Left" ,
317+ position : { x : 309 , y : 664 } ,
318+ } ,
277319 ] ;
278320
279321 let visibleQuadrants = quadrants ;
@@ -283,98 +325,45 @@ export const Odontogram: React.FC<OdontogramProps> = ({
283325 visibleQuadrants = quadrants . slice ( 2 ) ;
284326 }
285327
286-
287-
288328 const handleHover = (
289329 name : string ,
290330 e : React . MouseEvent ,
291- placement : Placement = "right" // default
331+ placement : Placement = "right" , // default
292332 ) => {
293333 const target = e . currentTarget as SVGGElement ;
294334 const path = target . querySelector ( "path" ) ;
295335
296- if ( ! path || ! svgRef . current ) return ;
336+ if ( ! ( path && svgRef . current ) ) {
337+ return ;
338+ }
297339
298340 const toothBox = path . getBoundingClientRect ( ) ;
299341 const svgBox = svgRef . current . getBoundingClientRect ( ) ;
300342
301343 const margin = tooltip ?. margin || 10 ; // distance between tooth and tooltip
302344
303345 // Compute tooltip position just above or below depending on space
304- let x = toothBox . left
305- let y = toothBox . top
306-
307- switch ( placement ) {
308- case "top" :
309- x = toothBox . left + toothBox . width / 2 ;
310- y = toothBox . top - margin ;
311- break ;
312- case "top-start" :
313- x = toothBox . left ;
314- y = toothBox . top - margin ;
315- break ;
316- case "top-end" :
317- x = toothBox . right ;
318- y = toothBox . top - margin ;
319- break ;
320- case "bottom" :
321- x = toothBox . left + toothBox . width / 2 ;
322- y = toothBox . bottom + margin ;
323- break ;
324- case "bottom-start" :
325- x = toothBox . left ;
326- y = toothBox . bottom + margin ;
327- break ;
328- case "bottom-end" :
329- x = toothBox . right ;
330- y = toothBox . bottom + margin ;
331- break ;
332- case "left" :
333- x = toothBox . left - margin ;
334- y = toothBox . top + toothBox . height / 2 ;
335- break ;
336- case "left-start" :
337- x = toothBox . left - margin ;
338- y = toothBox . top ;
339- break ;
340- case "left-end" :
341- x = toothBox . left - margin ;
342- y = toothBox . bottom ;
343- break ;
344- case "right" :
345- x = toothBox . right + margin ;
346- y = toothBox . top + toothBox . height / 2 ;
347- break ;
348- case "right-start" :
349- x = toothBox . right + margin ;
350- y = toothBox . top ;
351- break ;
352- case "right-end" :
353- x = toothBox . right + margin ;
354- y = toothBox . bottom ;
355- break ;
356- }
357346
347+ const { x, y } =
348+ placements [ placement ] ?.( toothBox , margin ) ??
349+ placements . right ( toothBox , margin ) ;
358350
359- // If tooltip would go above svg, place it below instead
360- if ( y < svgBox . top ) {
361- y = toothBox . bottom + margin ;
362- }
351+ const safeY = y < svgBox . top ? toothBox . bottom + margin : y ;
363352
364353 setTooltipData ( {
365354 active : true ,
366- position : { x, y } ,
355+ position : { x, y : safeY } ,
367356 payload : {
368357 id : name ,
369358 notations : getToothNotations ( name ) ,
370- type : teethPaths . find ( ( t ) => t . name === name . replace ( "teeth-" , "" ) . slice ( 1 ) )
371- ?. type ?? "Unknown" ,
359+ type :
360+ teethPaths . find ( ( t ) => t . name === name . replace ( "teeth-" , "" ) . slice ( 1 ) )
361+ ?. type ?? "Unknown" ,
372362 } ,
373363 } ) ;
374364 } ;
375365 const handleLeave = ( ) => setTooltipData ( ( p ) => ( { ...p , active : false } ) ) ;
376366
377-
378367 const renderTeeth = ( prefix : string ) =>
379368 teethPaths . map ( ( tooth ) => {
380369 const id = `${ prefix } ${ tooth . name } ` ;
@@ -396,14 +385,13 @@ export const Odontogram: React.FC<OdontogramProps> = ({
396385 ) ;
397386 } ) ;
398387
399-
400388 const finalColors = { ...themeColors , ...mapToCssVars ( colors ) } ;
401389
402390 return (
403391 < div
404392 className = { `Odontogram ${ theme === "dark" ? "dark-theme" : "" } ` }
405393 style = { {
406- ...( finalColors as React . CSSProperties ) ,
394+ ...( finalColors as CSSProperties ) ,
407395 width : "100%" ,
408396 maxWidth : 300 ,
409397 margin : "0 auto" ,
@@ -417,7 +405,13 @@ export const Odontogram: React.FC<OdontogramProps> = ({
417405 ref = { svgRef }
418406 xmlns = "http://www.w3.org/2000/svg"
419407 fill = "none"
420- viewBox = { showHalf === "full" ? "0 0 409 694" : showHalf === 'upper' ? "0 0 409 347" : "0 200 409 694" }
408+ viewBox = {
409+ showHalf === "full"
410+ ? "0 0 409 694"
411+ : showHalf === "upper"
412+ ? "0 0 409 347"
413+ : "0 200 409 694"
414+ }
421415 className = "Odontogram"
422416 style = { {
423417 width : "100%" ,
@@ -426,11 +420,10 @@ export const Odontogram: React.FC<OdontogramProps> = ({
426420 touchAction : "manipulation" ,
427421 } }
428422 >
429-
423+ < title > Odontogram </ title >
430424 { visibleQuadrants . map ( ( { name, transform, label, position } , index ) => (
431425 < g key = { name } name = { name } transform = { transform } >
432426 { renderTeeth ( `teeth-${ index + 1 } ` ) }
433-
434427 </ g >
435428 ) ) }
436429 </ svg >
0 commit comments