Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 인터랙션 키보드 접근성 개선 #128

Merged
merged 11 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="소프티어 부트캠프 4기-팀 어썸 오렌지의 IONIQ 5 어드민 페이지입니다." />
<link rel="preload" href="/font/HyundaiSansTextKROTFBold.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFMedium.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFRegular.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFBold.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFMedium.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFRegular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="/font/fonts.css" as="style" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
<title>Awesome Orange - Admin</title>
</head>
<body>
Expand Down
7 changes: 4 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="HI. Five. The new IONIQ 5. 소프티어 부트캠프 4기-팀 어썸 오렌지의 IONIQ 5 이벤트 페이지입니다." />
<link rel="preload" href="/font/DS-DIGI.TTF" as="font" type="font/ttf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFBold.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFMedium.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFRegular.otf" as="font" type="font/otf" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFBold.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFMedium.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="/font/HyundaiSansTextKROTFRegular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="/font/fonts.css" as="style" crossorigin="anonymous" onload="this.onload=null;this.rel='stylesheet'">
<title>Awesome Orange FE</title>
</head>
<body>
Expand Down
Binary file added public/font/HyundaiSansTextKROTFBold.woff2
Binary file not shown.
Binary file added public/font/HyundaiSansTextKROTFMedium.woff2
Binary file not shown.
Binary file added public/font/HyundaiSansTextKROTFRegular.woff2
Binary file not shown.
20 changes: 20 additions & 0 deletions public/font/fonts.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@font-face {
font-family: "hdsans";
src: url("/font/HyundaiSansTextKROTFBold.woff2") format("woff2");
font-weight: bold;
font-display: swap;
}

@font-face {
font-family: "hdsans";
src: url("/font/HyundaiSansTextKROTFMedium.woff2") format("woff2");
font-weight: 500;
font-display: swap;
}

@font-face {
font-family: "hdsans";
src: url("/font/HyundaiSansTextKROTFRegular.woff2") format("woff2");
font-weight: 400;
font-display: swap;
}
14 changes: 10 additions & 4 deletions src/adminPage/features/eventDetail/drawButton/DrawResultModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Fragment, useMemo } from "react";
import useScrollControl from "./useScrollControl.js";
import { fetchServer } from "@common/dataFetch/fetchServer.js";
import { useQuery } from "@common/dataFetch/getQuery.js";
import { GroupMap } from "@common/utils.js";
import { GroupMap, addHyphen } from "@common/utils.js";

function mapResultSubsetGroup({ ranking, name, phoneNumber }) {
return (
<Fragment key={`${ranking}-${name}-${phoneNumber}`}>
<p className="font-medium">{ranking}등</p>
<p>{name}</p>
<p>{phoneNumber}</p>
<p>{addHyphen(phoneNumber)}</p>
</Fragment>
);
}
Expand All @@ -21,7 +21,7 @@ function DrawResultModal({ eventId }) {
const { hullRef, mountMap, scrollTo, intersectState } = useScrollControl();

// render logic
const maxGrade = drawResultData.at(-1).ranking;
const maxGrade = drawResultData.length === 0 ? 0 : drawResultData.at(-1).ranking;
const tableStyle =
"w-full grid grid-cols-[4rem_1fr_2fr] auto-rows-[minmax(2rem,auto)] gap-4 items-center justify-items-center";

Expand Down Expand Up @@ -63,7 +63,13 @@ function DrawResultModal({ eventId }) {
<p>전화번호</p>
</div>
<div className="w-full flex-grow overflow-y-scroll" ref={hullRef}>
<div className="flex flex-col gap-2">{[...drawResultGroup].map(mapResultGroup)}</div>
{drawResultData.length === 0 ? (
<div className="w-full h-full flex justify-center items-center text-body-l font-bold text-neutral-600">
저런! 참가자가 없군요!
</div>
) : (
<div className="flex flex-col gap-2">{[...drawResultGroup].map(mapResultGroup)}</div>
)}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function useScrollControl() {
},
{ root: hullRef.current ?? null, threshold: 0.01 },
);
for (let [, elem] of itemRef.current) {
for (let [, elem] of getMap()) {
observerRef.current.observe(elem);
}

Expand Down
27 changes: 0 additions & 27 deletions src/adminPage/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,6 @@
@tailwind components;
@tailwind utilities;

@font-face {
font-family: "ds-digital";
src: url("/font/DS-DIGI.TTF") format("truetype");
font-display: swap;
}

@font-face {
font-family: "hdsans";
src: url("/font/HyundaiSansTextKROTFBold.otf") format("opentype");
font-weight: bold;
font-display: swap;
}

@font-face {
font-family: "hdsans";
src: url("/font/HyundaiSansTextKROTFMedium.otf") format("opentype");
font-weight: 500;
font-display: swap;
}

@font-face {
font-family: "hdsans";
src: url("/font/HyundaiSansTextKROTFRegular.otf") format("opentype");
font-weight: 400;
font-display: swap;
}

@layer base {
body {
font-family: "hdsans", sans-serif;
Expand Down
9 changes: 1 addition & 8 deletions src/common/components/PhoneInput.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import Input from "./Input.jsx";
import { addHyphen } from "../utils.js";

function PhoneInput({ text, setText, ...otherProps }) {
function addHyphen(value) {
const plain = value.replace(/\D/g, "");

if (plain.length < 4) return plain;
if (plain.length <= 7) return plain.replace(/^(\d{3})(\d{0,4})$/, "$1-$2");
if (plain.length <= 10) return plain.replace(/^(\d{3})(\d{3})(\d{0,4})$/, "$1-$2-$3");
return plain.replace(/^(\d{3})(\d{4})(\d{4,})$/, "$1-$2-$3");
}
return (
<Input
text={text}
Expand Down
9 changes: 9 additions & 0 deletions src/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,12 @@ export function getDayDifference(_date1, _date2) {

return dayDifference;
}

export function addHyphen(value) {
const plain = value.replace(/\D/g, "");

if (plain.length < 4) return plain;
if (plain.length <= 7) return plain.replace(/^(\d{3})(\d{0,4})$/, "$1-$2");
if (plain.length <= 10) return plain.replace(/^(\d{3})(\d{3})(\d{0,4})$/, "$1-$2-$3");
return plain.replace(/^(\d{3})(\d{4})(\d{4,})$/, "$1-$2-$3");
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function InteractionSlide({ interactionDesc, index, isCurrent, sl
</span>

<div className="pt-5 flex items-center">
<img src={numberImgPath} alt="" role="presentation" />
<img src={numberImgPath} alt="" role="presentation" width="24" height="24" />

<span
className={`${isCurrent ? "opacity-100" : "opacity-50"} pl-3 text-title-m sm:text-head-s text-white font-bold`}
Expand Down
2 changes: 2 additions & 0 deletions src/mainPage/features/interactions/description/TapBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ function TabBarItem({ currentInteraction, slideTo, index }) {
alt="체크"
className={`${(!isJoined || isInvisible) && "invisible"}`}
draggable="false"
width="40"
height="40"
/>

<span
Expand Down
29 changes: 21 additions & 8 deletions src/mainPage/features/interactions/distanceDriven/index.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useImperativeHandle } from "react";
import { useEffect, useImperativeHandle } from "react";
import InteractionDescription from "../InteractionDescription.jsx";
import AnswerText from "./AnswerText.jsx";
import usePointDrag from "./usePointDrag.js";
import useDeviceRatio from "./useDeviceRatio.js";

function DistanceDrivenInteraction({ interactCallback, $ref }) {
const { x, y, reset, isDragging, onPointerDown } = usePointDrag();
const MAX_DISTANCE = 800;

function DistanceDrivenInteraction({ interactCallback, $ref, disabled }) {
const { x, y, reset, isDragging, onPointerDown, handleRef, subtitle } = usePointDrag(!disabled);
const ratio = useDeviceRatio();
const km = Math.floor((Math.hypot(x, y) * MAX_DISTANCE) / ratio);

const circleStyle = {
transform: `translate(${x}px, ${y}px)`,
Expand All @@ -25,6 +29,10 @@ function DistanceDrivenInteraction({ interactCallback, $ref }) {
);
}

useEffect(() => {
if (km !== 0) interactCallback?.();
}, [km, interactCallback]);

useImperativeHandle($ref, () => ({ reset }), [reset]);

return (
Expand All @@ -36,15 +44,22 @@ function DistanceDrivenInteraction({ interactCallback, $ref }) {
directive="가운데 점을 드래그하여 최대 주행거리를 예측해보세요!"
shouldNotSelect={isDragging}
/>
<span aria-live="assertive" className="assistive-text">
{subtitle(x, y, km)}
</span>
<span aria-live="assertive" className="assistive-text">
스페이스바를 눌러서 드래그 상태를 전환하세요.
</span>
<div className="absolute top-1/2">
<div
tabIndex={disabled ? undefined : 0}
className="rounded-full size-8 bg-blue-500 cursor-pointer touch-none before:size-8 before:rounded-full before:absolute before:left-0 before:top-0 before:z-10 before:bg-blue-500 before:opacity-50"
onPointerDown={(e) => {
onPointerDown(e);
pulseAnimation(e);
interactCallback?.();
}}
style={circleStyle}
ref={handleRef}
/>
<svg
className="overflow-visible stroke-blue-500 absolute top-4 left-4 pointer-events-none"
Expand All @@ -55,9 +70,7 @@ function DistanceDrivenInteraction({ interactCallback, $ref }) {
</svg>
</div>
<p className="text-white absolute bottom-32 md:bottom-36 lg:bottom-[180px] text-title-s font-bold pointer-events-none">
<span className="text-head-m md:text-head-l lg:text-17.5 mr-1.5 lg:mr-2.5">
<AnswerText distance={Math.hypot(x, y)} />
</span>
<span className="text-head-m md:text-head-l lg:text-17.5 mr-1.5 lg:mr-2.5">{km}</span>
km
</p>
</article>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { useState, useEffect } from "react";
import throttleRaf from "@common/throttleRaf.js";

const MAX_ANSWER = 800;

function AnswerText({ distance }) {
function useDeviceRatio() {
const [ratio, setRatio] = useState(1);
useEffect(() => {
setRatio(Math.hypot(window.innerWidth, window.innerHeight) / 2);
const onResize = throttleRaf(() => {
setRatio(Math.hypot(window.innerWidth, window.innerHeight) / (2 * MAX_ANSWER));
setRatio(Math.hypot(window.innerWidth, window.innerHeight) / 2);
});
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);

return <>{Math.round(distance / ratio)}</>;
return ratio;
}

export default AnswerText;
export default useDeviceRatio;
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { useState, useRef, useCallback } from "react";
import useMountDragEvent from "@main/hooks/useMountDragEvent.js";
import useA11yDrag from "@main/hooks/useA11yDrag.js";

function usePointDrag() {
const grabText = (x, y, km) =>
`점을 잡았습니다. 현재 좌표는 (${x}, ${y})이며, 거리는 ${km}km입니다. 방향키를 눌러 점의 위치를 조정하세요. 스페이스바를 눌러 점을 놓을 수 있습니다.`;
const moveText = (x, y, km) => `현재 좌표는 (${x}, ${y})이며, 거리는 ${km}km입니다.`;
const dropText = (x, y, km) => `점이 놓였습니다. 새 좌표는 (${x}, ${y})이며, 거리는 ${km}km입니다.`;

function usePointDrag(enabled) {
const prevState = useRef({ x: 0, y: 0, mouseX: 0, mouseY: 0 });
const [x, setX] = useState(0);
const [y, setY] = useState(0);
const [subtitle, setSubtitle] = useState(() => () => "");

const onDragStart = useCallback(
function ({ x: mouseX, y: mouseY }) {
Expand All @@ -17,9 +24,24 @@ function usePointDrag() {
setY(prevState.current.y + mouse.y - prevState.current.mouseY);
}, []);

const onKeyMove = useCallback(function (x, y) {
setX((prev) => prev + x * 10);
setY((prev) => prev + y * 10);
}, []);

const { onPointerDown, dragState } = useMountDragEvent({
onDragStart,
onDrag,
enabled,
});

const handleRef = useA11yDrag({
grabText,
moveText,
dropText,
onKeyMove,
enabled,
setSubtitle,
});

return {
Expand All @@ -31,6 +53,8 @@ function usePointDrag() {
},
isDragging: dragState,
onPointerDown,
handleRef,
subtitle,
};
}

Expand Down
32 changes: 23 additions & 9 deletions src/mainPage/features/interactions/fastCharge/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useImperativeHandle } from "react";
import { useEffect, useImperativeHandle } from "react";
import InteractionDescription from "../InteractionDescription.jsx";
import BatteryProgressBar from "./BatteryProgressBar.jsx";
import dialSvg from "./assets/timer.svg";
Expand All @@ -13,18 +13,25 @@ function getProgress(angle) {
return rawProgress;
}

function FastChargeInteraction({ interactCallback, $ref }) {
function FastChargeInteraction({ interactCallback, $ref, disabled }) {
const {
angle,
style: dialStyle,
ref: dialRef,
keyRef,
onPointerDown,
resetAngle: reset,
isDragging,
} = useDialDrag(0);
subtitle,
} = useDialDrag(!disabled);

useEffect(() => {
if (angle !== 0) interactCallback?.();
}, [angle, interactCallback]);

useImperativeHandle($ref, () => ({ reset }), [reset]);
const progress = getProgress(angle);
const answer = Math.round(progress * MAX_MINUTE);

return (
<article className="relative w-full h-full overflow-hidden flex items-center flex-col">
Expand All @@ -35,6 +42,12 @@ function FastChargeInteraction({ interactCallback, $ref }) {
directive="다이얼을 돌려 충전에 필요한 시간을 확인해보세요!"
shouldNotSelect={isDragging}
/>
<span aria-live="assertive" className="assistive-text">
{subtitle(answer, angle)}
</span>
<span aria-live="assertive" className="assistive-text">
스페이스바를 눌러서 다이얼 조작 여부를 전환하세요.
</span>
<div className="absolute top-[clamp(240px,40%,384px)] w-72 md:w-96 h-32 border-solid border-2 border-neutral-600 rounded-[30px] p-3.5">
<div className="absolute w-5 h-9 bg-neutral-600 right-[-1.25rem] top-[2.875rem] rounded-r-md"></div>
<BatteryProgressBar progress={progress} />
Expand All @@ -46,15 +59,16 @@ function FastChargeInteraction({ interactCallback, $ref }) {
className="w-full h-full absolute left-0 top-0 cursor-pointer touch-none select-none"
style={dialStyle}
ref={dialRef}
onPointerDown={(e) => {
onPointerDown(e);
interactCallback?.();
}}
onPointerDown={onPointerDown}
draggable="false"
/>
<p className="text-white font-bold absolute bottom-[calc(50%+48px)] md:bottom-[calc(50%+94px)] lg:bottom-[calc(50%+140px)] text-title-s pointer-events-none">
<span className="text-head-m md:text-head-l lg:text-17.5 mr-1.5 lg:mr-2.5">
{Math.round(progress * MAX_MINUTE)}
<span
className="text-head-m md:text-head-l lg:text-17.5 mr-1.5 lg:mr-2.5"
tabIndex={disabled ? undefined : 0}
ref={keyRef}
>
{answer}
</span>
</p>
Expand Down
Loading