From 3df3f7aaf54f5fe769c821a38c77402208458d92 Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Wed, 6 Mar 2024 16:24:32 +0000 Subject: [PATCH 01/15] chore: set a `color-scheme` for the docs this fixes the logo on the README being invisible when the client is in dark mode --- .storybook/preview-head.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 80ea188e..ee22169c 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -2,3 +2,8 @@ href="https://googleapis-fonts.thenational.academy/css2?family=Lexend:wght@300;400;600&display=swap" rel="stylesheet" /> + From c0ca371cc86692d3fc38528934d6c96276bc9f6a Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Wed, 6 Mar 2024 16:27:21 +0000 Subject: [PATCH 02/15] chore: correct name of directory for OakQuizOrder --- .../OakQuizOrder.stories.tsx | 2 +- .../OakQuizOrder.test.tsx | 0 .../{OakQuizOrderQuestion => OakQuizOrder}/OakQuizOrder.tsx | 0 .../__snapshots__/OakQuizOrder.test.tsx.snap | 0 .../pupil/{OakQuizOrderQuestion => OakQuizOrder}/index.ts | 0 src/components/organisms/pupil/index.ts | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename src/components/organisms/pupil/{OakQuizOrderQuestion => OakQuizOrder}/OakQuizOrder.stories.tsx (95%) rename src/components/organisms/pupil/{OakQuizOrderQuestion => OakQuizOrder}/OakQuizOrder.test.tsx (100%) rename src/components/organisms/pupil/{OakQuizOrderQuestion => OakQuizOrder}/OakQuizOrder.tsx (100%) rename src/components/organisms/pupil/{OakQuizOrderQuestion => OakQuizOrder}/__snapshots__/OakQuizOrder.test.tsx.snap (100%) rename src/components/organisms/pupil/{OakQuizOrderQuestion => OakQuizOrder}/index.ts (100%) diff --git a/src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.stories.tsx b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.stories.tsx similarity index 95% rename from src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.stories.tsx rename to src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.stories.tsx index c17f4dac..8e465bc6 100644 --- a/src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.stories.tsx +++ b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.stories.tsx @@ -6,7 +6,7 @@ import { OakQuizOrder } from "./OakQuizOrder"; const meta: Meta = { component: OakQuizOrder, tags: ["autodocs"], - title: "components/organisms/pupil/OakQuizOrderQuestion", + title: "components/organisms/pupil/OakQuizOrder", parameters: { controls: { include: [], diff --git a/src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.test.tsx b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx similarity index 100% rename from src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.test.tsx rename to src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx diff --git a/src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.tsx b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx similarity index 100% rename from src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.tsx rename to src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx diff --git a/src/components/organisms/pupil/OakQuizOrderQuestion/__snapshots__/OakQuizOrder.test.tsx.snap b/src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap similarity index 100% rename from src/components/organisms/pupil/OakQuizOrderQuestion/__snapshots__/OakQuizOrder.test.tsx.snap rename to src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap diff --git a/src/components/organisms/pupil/OakQuizOrderQuestion/index.ts b/src/components/organisms/pupil/OakQuizOrder/index.ts similarity index 100% rename from src/components/organisms/pupil/OakQuizOrderQuestion/index.ts rename to src/components/organisms/pupil/OakQuizOrder/index.ts diff --git a/src/components/organisms/pupil/index.ts b/src/components/organisms/pupil/index.ts index 1a315aeb..caa869f3 100644 --- a/src/components/organisms/pupil/index.ts +++ b/src/components/organisms/pupil/index.ts @@ -11,4 +11,4 @@ export * from "./OakHintButton"; export * from "./OakLessonNavItem"; export * from "./OakLessonReviewItem"; export * from "./OakLessonVideoTranscript"; -export * from "./OakQuizOrderQuestion"; +export * from "./OakQuizOrder"; From 98875b3329cad32dc9c0a97e5ba89be96477baf4 Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Wed, 6 Mar 2024 16:56:13 +0000 Subject: [PATCH 03/15] refactor(PUPIL-443): extract `InternalDndContext` and `usePrefersReducedMotion` we'll need these for the match question --- src/animation/usePrefersReducedMotion.test.ts | 22 ++++++++++ src/animation/usePrefersReducedMotion.ts | 16 +++++++ .../InternalDndContext/InternalDndContext.tsx | 30 +++++++++++++ .../atoms/InternalDndContext/index.ts | 0 .../pupil/OakQuizOrder/OakQuizOrder.test.tsx | 3 +- .../pupil/OakQuizOrder/OakQuizOrder.tsx | 43 +++---------------- 6 files changed, 76 insertions(+), 38 deletions(-) create mode 100644 src/animation/usePrefersReducedMotion.test.ts create mode 100644 src/animation/usePrefersReducedMotion.ts create mode 100644 src/components/atoms/InternalDndContext/InternalDndContext.tsx create mode 100644 src/components/atoms/InternalDndContext/index.ts diff --git a/src/animation/usePrefersReducedMotion.test.ts b/src/animation/usePrefersReducedMotion.test.ts new file mode 100644 index 00000000..eb8fbaa2 --- /dev/null +++ b/src/animation/usePrefersReducedMotion.test.ts @@ -0,0 +1,22 @@ +import { renderHook } from "@testing-library/react"; + +import { usePrefersReducedMotion } from "./usePrefersReducedMotion"; + +window.matchMedia = jest.fn().mockReturnValue({ + matches: false, +}); + +describe(usePrefersReducedMotion, () => { + it("is true when the media query matches", () => { + jest.spyOn(window, "matchMedia").mockReturnValue({ + matches: true, + } as MediaQueryList); + + const { result } = renderHook(() => usePrefersReducedMotion()); + + expect(window.matchMedia).toHaveBeenCalledWith( + "(prefers-reduced-motion: reduce)", + ); + expect(result.current).toBe(true); + }); +}); diff --git a/src/animation/usePrefersReducedMotion.ts b/src/animation/usePrefersReducedMotion.ts new file mode 100644 index 00000000..2575af69 --- /dev/null +++ b/src/animation/usePrefersReducedMotion.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; + +/** + * Returns true if the user has requested that the system minimize the amount of non-essential motion it uses. + */ +export function usePrefersReducedMotion() { + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); + + setPrefersReducedMotion(mediaQuery.matches); + }, []); + + return prefersReducedMotion; +} diff --git a/src/components/atoms/InternalDndContext/InternalDndContext.tsx b/src/components/atoms/InternalDndContext/InternalDndContext.tsx new file mode 100644 index 00000000..5d8dcf12 --- /dev/null +++ b/src/components/atoms/InternalDndContext/InternalDndContext.tsx @@ -0,0 +1,30 @@ +import { DndContext, DndContextProps } from "@dnd-kit/core"; +import React, { FC, createContext, useContext, useEffect } from "react"; + +/** + * Facilitates DI for the DndContext + */ +export const injectDndContext = createContext>(DndContext); + +/** + * Wraps dnd-kit's `DndContext` to normalise scroll behaviour and enable dependency injection + */ +export const InternalDndContext = (props: DndContextProps) => { + const DndContext = useContext(injectDndContext); + + /** + * Disable smooth scrolling during drag to ensure that the dragged item is always visible + */ + useEffect(() => { + const originalScrollingBehaviour = + document.documentElement.style.scrollBehavior; + document.documentElement.style.scrollBehavior = "auto"; + + return () => { + document.documentElement.style.scrollBehavior = + originalScrollingBehaviour; + }; + }); + + return ; +}; diff --git a/src/components/atoms/InternalDndContext/index.ts b/src/components/atoms/InternalDndContext/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx index dad700c1..f81f4d1b 100644 --- a/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx +++ b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx @@ -3,10 +3,11 @@ import { ThemeProvider } from "styled-components"; import React from "react"; import { DndContext, DndContextProps, UniqueIdentifier } from "@dnd-kit/core"; -import { OakQuizOrder, injectDndContext } from "./OakQuizOrder"; +import { OakQuizOrder } from "./OakQuizOrder"; import { oakDefaultTheme } from "@/styles"; import renderWithTheme from "@/test-helpers/renderWithTheme"; +import { injectDndContext } from "@/components/atoms/InternalDndContext/InternalDndContext"; window.matchMedia = jest.fn().mockReturnValue({ matches: false, diff --git a/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx index 8b5e45f3..f6e66f9c 100644 --- a/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx +++ b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx @@ -1,12 +1,5 @@ -import React, { - FC, - createContext, - useContext, - useEffect, - useState, -} from "react"; +import React, { useState } from "react"; import { - DndContext, closestCenter, KeyboardSensor, useSensor, @@ -16,7 +9,6 @@ import { DragStartEvent, UniqueIdentifier, Announcements, - DndContextProps, MouseSensor, TouchSensor, } from "@dnd-kit/core"; @@ -36,6 +28,8 @@ import { OakDraggable, OakDroppable, } from "@/components/molecules"; +import { InternalDndContext } from "@/components/atoms/InternalDndContext/InternalDndContext"; +import { usePrefersReducedMotion } from "@/animation/usePrefersReducedMotion"; type OakQuizOrderItem = { id: string; @@ -92,11 +86,6 @@ const ConnectedDraggable = ({ id, label }: OakQuizOrderItem) => { ); }; -/** - * Facilitates DI for the DndContext - */ -export const injectDndContext = createContext>(DndContext); - /** * A sortable list of items with drag and drop functionality. Items can be dragged over named slots to re-arrange them * @@ -106,39 +95,19 @@ export const OakQuizOrder = ({ initialItems, onChange }: OakQuizOrderProps) => { const [items, setItems] = useState(initialItems); const [activeId, setActiveId] = useState(null); const activeItem = items.find((item) => item.id === activeId); - const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); const sensors = useSensors( useSensor(MouseSensor), useSensor(TouchSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, - scrollBehavior: prefersReducedMotion ? "instant" : "smooth", + scrollBehavior: usePrefersReducedMotion() ? "instant" : "smooth", }), ); - const DndContext = useContext(injectDndContext); - - /** - * Disable smooth scrolling during drag to ensure that the dragged item is always visible - */ - useEffect(() => { - const originalScrollingBehaviour = - document.documentElement.style.scrollBehavior; - document.documentElement.style.scrollBehavior = "auto"; - - setPrefersReducedMotion( - window.matchMedia("(prefers-reduced-motion)").matches, - ); - - return () => { - document.documentElement.style.scrollBehavior = - originalScrollingBehaviour; - }; - }); return ( <> - { document.body, )} - + ); From c69c5b909e006ee4838827e4fb70dfe91d6aaa8c Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Thu, 7 Mar 2024 14:52:20 +0000 Subject: [PATCH 04/15] fix: allow additional spacing tokens for $flexBasis --- src/styles/utils/flexStyle.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/styles/utils/flexStyle.ts b/src/styles/utils/flexStyle.ts index 7064613c..0892cacf 100644 --- a/src/styles/utils/flexStyle.ts +++ b/src/styles/utils/flexStyle.ts @@ -9,6 +9,7 @@ import { } from "@/styles/utils/responsiveStyle"; import { OakAllSpacingToken, + OakCombinedSpacingToken, OakSpaceBetweenToken, } from "@/styles/theme/spacing"; import { parseSpacing } from "@/styles/helpers/parseSpacing"; @@ -73,9 +74,7 @@ export type FlexStyleProps = DisplayStyleProps & { * * Accepts a spacing token or a responsive array of spacing tokens. Can be nulled. */ - $flexBasis?: ResponsiveValues< - OakAllSpacingToken | OakSpaceBetweenToken | null | undefined - >; + $flexBasis?: ResponsiveValues; /** * Sets the `gap` CSS property of the element. * From 200c2a0a50df5f2f8403a27cb2690e779a1852e9 Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Thu, 7 Mar 2024 15:34:08 +0000 Subject: [PATCH 05/15] feat(PUPIL-442): add a slot for a label to the droppable component --- .../OakDroppable/OakDroppable.stories.tsx | 44 ++++++++++++- .../molecules/OakDroppable/OakDroppable.tsx | 61 +++++++++++++------ 2 files changed, 83 insertions(+), 22 deletions(-) diff --git a/src/components/molecules/OakDroppable/OakDroppable.stories.tsx b/src/components/molecules/OakDroppable/OakDroppable.stories.tsx index 09f1b24c..5ca03052 100644 --- a/src/components/molecules/OakDroppable/OakDroppable.stories.tsx +++ b/src/components/molecules/OakDroppable/OakDroppable.stories.tsx @@ -11,11 +11,12 @@ const meta: Meta = { title: "components/molecules/OakDroppable", argTypes: { children: { type: "string" }, + labelSlot: { type: "string" }, isOver: { type: "boolean" }, }, parameters: { controls: { - include: ["children", "isOver"], + include: ["children", "labelSlot", "isOver"], }, backgrounds: { default: "light", @@ -29,7 +30,10 @@ type Story = StoryObj; export const Default: Story = {}; -export const ADraggableHasEnteredTheDroppable: Story = { +/** + * A draggable has entered the droppable so it has entered an active state + */ +export const DraggingOver: Story = { args: { isOver: true, }, @@ -40,3 +44,39 @@ export const Occupied: Story = { children: Elephant, }, }; + +export const WithSlotLabel: Story = { + args: { + labelSlot: "never forgets", + }, +}; + +export const OccupiedWithSlotLabel: Story = { + args: { + labelSlot: "never forgets", + children: Elephant, + }, +}; +/** + * A draggable has entered the droppable so it has entered an active state + */ +export const DraggingOverWithSlotLabel: Story = { + args: { + isOver: true, + labelSlot: "never forgets", + }, +}; + +export const WithLongSlotLabel: Story = { + args: { + labelSlot: + "which animal never forgets and is the largest land animal on earth?", + }, +}; + +export const WithAVeryLongSlotLabel: Story = { + args: { + labelSlot: + "which animal is the largest land mammal with a long trunk, large ears, and tusks? Known for intelligence and social behavior, it symbolizes strength and conservation efforts worldwide.", + }, +}; diff --git a/src/components/molecules/OakDroppable/OakDroppable.tsx b/src/components/molecules/OakDroppable/OakDroppable.tsx index bddc5d4d..8f2ccd22 100644 --- a/src/components/molecules/OakDroppable/OakDroppable.tsx +++ b/src/components/molecules/OakDroppable/OakDroppable.tsx @@ -7,7 +7,7 @@ import React, { } from "react"; import styled from "styled-components"; -import { OakBox } from "@/components/atoms"; +import { OakBox, OakFlex } from "@/components/atoms"; import { parseBorder } from "@/styles/helpers/parseBorder"; import { parseColor } from "@/styles/helpers/parseColor"; @@ -16,18 +16,21 @@ export type OakDroppableProps = { * Indicates whether a draggable is currently being dragged over the droppable */ isOver?: boolean; + /** + * A slot for a label to be displayed to the RHS of the droppable + * + * useful for giving the user a hint about what to drop + */ + labelSlot?: ReactNode; + /** + * A slot for the draggable that is currently occupying the droppable + */ children?: ReactNode; }; -const StyledBox = styled(OakBox)` +const StyledFlex = styled(OakFlex)` outline: ${parseBorder("border-solid-l")} ${parseColor("border-primary")}; outline-style: dashed; - - @media (hover: hover) { - &:hover { - background-color: ${parseColor("bg-primary")}; - } - } `; /** @@ -37,28 +40,46 @@ const StyledBox = styled(OakBox)` * It is intended to be used with `useDraggable` from `@dnd-kit/core` */ export const OakDroppable: FC< - OakDroppableProps & ComponentPropsWithRef + OakDroppableProps & ComponentPropsWithRef > = forwardRef< HTMLDivElement, - OakDroppableProps & ComponentPropsWithoutRef ->(({ children, isOver, ...props }, ref) => { + OakDroppableProps & ComponentPropsWithoutRef +>(({ children, labelSlot, isOver, ...props }, ref) => { return ( - - - {children} - - + {children} + + {labelSlot && ( + + {labelSlot} + + )} + ); }); From 1ca2dfa8191d2eabab4cfde6592c8380cf65907d Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Thu, 7 Mar 2024 16:28:15 +0000 Subject: [PATCH 06/15] feat(PUPIL-442): add some color options to OakDraggable --- .../OakDraggable/OakDraggable.stories.tsx | 8 + .../molecules/OakDraggable/OakDraggable.tsx | 61 ++- .../__snapshots__/OakDraggable.test.tsx.snap | 19 +- .../__snapshots__/OakDroppable.test.tsx.snap | 61 ++- .../__snapshots__/OakQuizOrder.test.tsx.snap | 418 ++++++++++-------- 5 files changed, 365 insertions(+), 202 deletions(-) diff --git a/src/components/molecules/OakDraggable/OakDraggable.stories.tsx b/src/components/molecules/OakDraggable/OakDraggable.stories.tsx index ae9805c1..c8d962cd 100644 --- a/src/components/molecules/OakDraggable/OakDraggable.stories.tsx +++ b/src/components/molecules/OakDraggable/OakDraggable.stories.tsx @@ -48,3 +48,11 @@ export const ReadOnly: Story = { isReadOnly: true, }, }; + +export const WithColors: Story = { + args: { + color: "text-inverted", + background: "bg-btn-primary", + iconColor: "icon-main", + }, +}; diff --git a/src/components/molecules/OakDraggable/OakDraggable.tsx b/src/components/molecules/OakDraggable/OakDraggable.tsx index 83411c66..e70dc50a 100644 --- a/src/components/molecules/OakDraggable/OakDraggable.tsx +++ b/src/components/molecules/OakDraggable/OakDraggable.tsx @@ -12,7 +12,9 @@ import { parseBorder } from "@/styles/helpers/parseBorder"; import { parseDropShadow } from "@/styles/helpers/parseDropShadow"; import { parseSpacing } from "@/styles/helpers/parseSpacing"; import { IconName } from "@/image-map"; -import { OakColorFilterToken } from "@/styles/theme/color"; +import { OakCombinedColorToken } from "@/styles/theme/color"; +import { parseBorderWidth } from "@/styles/helpers/parseBorderWidth"; +import { parseColorFilter } from "@/styles/helpers/parseColorFilter"; type OakDraggableProps = { /** @@ -34,23 +36,45 @@ type OakDraggableProps = { */ iconName?: IconName; /** - * Icon color + * Icon color when not being dragged or hovered */ - iconColor?: OakColorFilterToken; + iconColor?: OakCombinedColorToken; + /** + * The background color of the draggable when not being dragged or hovered + */ + background?: OakCombinedColorToken; + /** + * The color of the draggable when not being dragged or hovered + */ + color?: OakCombinedColorToken; }; -const StyledDraggable = styled(OakBox)` +const StyledOakIcon = styled(OakIcon)``; + +const StyledDraggable = styled(OakBox)<{ $iconColor: OakCombinedColorToken }>` cursor: grab; outline: none; + user-select: none; + + ${StyledOakIcon} { + filter: ${(props) => parseColorFilter(props.$iconColor)}; + } @media (hover: hover) { &:hover:not([data-dragging="true"]):not([data-disabled="true"]):not( [data-readonly="true"] ) { background-color: ${parseColor("bg-decorative1-subdued")}; + color: ${parseColor("text-primary")}; box-shadow: ${parseDropShadow("drop-shadow-standard")}; border-bottom: ${parseBorder("border-solid-xl")} ${parseColor("border-primary")}; + padding-bottom: ${parseSpacing("inner-padding-m")}; + text-decoration: underline; + + ${StyledOakIcon} { + filter: ${parseColorFilter("icon-inverted")}; + } } } @@ -62,15 +86,26 @@ const StyledDraggable = styled(OakBox)` &[data-dragging="true"] { cursor: move; background-color: ${parseColor("bg-decorative1-main")}; - border: ${parseBorder("border-solid-xl")} ${parseColor("border-primary")}; + color: ${parseColor("text-primary")}; + outline: ${parseBorder("border-solid-xl")} ${parseColor("border-primary")}; + outline-offset: -${parseBorderWidth("border-solid-xl")}; box-shadow: ${parseDropShadow("drop-shadow-lemon")}, ${parseDropShadow("drop-shadow-grey")}; + text-decoration: underline; + + ${StyledOakIcon} { + filter: ${parseColorFilter("icon-inverted")}; + } } &[data-disabled="true"] { cursor: default; background-color: ${parseColor("bg-neutral")}; color: ${parseColor("text-disabled")}; + + ${StyledOakIcon} { + filter: ${parseColorFilter("icon-disabled")}; + } } &[data-readonly="true"] { @@ -90,17 +125,18 @@ export const OakDraggable: FC< ComponentPropsWithRef > = forwardRef< HTMLDivElement, - ComponentPropsWithoutRef + OakDraggableProps & ComponentPropsWithoutRef >( ( { children, iconName = "move-arrows", - iconColor = "icon-primary", + iconColor = "icon-inverted", + color = "text-primary", + background = "bg-primary", isDragging, isDisabled, isReadOnly, - $borderColor = "transparent", ...props }, ref, @@ -111,20 +147,19 @@ export const OakDraggable: FC< $pv="inner-padding-l" $pl="inner-padding-s" $pr="inner-padding-m" - $background="bg-primary" + $background={background} + $color={color} $borderRadius="border-radius-m2" - $borderColor={$borderColor} - $bb="border-solid-xl" $minHeight="all-spacing-10" data-dragging={isDragging} data-disabled={isDisabled} data-readonly={isReadOnly} + $iconColor={iconColor} {...props} > -
- Children +
+ Children +
`; diff --git a/src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap b/src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap index f3b0bbf1..869c8ba5 100644 --- a/src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap +++ b/src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap @@ -77,7 +77,11 @@ exports[`OakQuizOrder matches snapshot 1`] = ` line-height: calc(1.5rem + 0.5rem); } -@media (hover:hover) { +@media (min-width:750px) { + +} + +@media (min-width:1280px) { } @@ -161,7 +165,7 @@ exports[`OakQuizOrder matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c9 { +.c12 { position: relative; width: 2rem; height: 2rem; @@ -171,33 +175,36 @@ exports[`OakQuizOrder matches snapshot 1`] = ` .c2 { padding: 1rem; background: #cee7e5; - border-radius: 0.375rem; + border-radius: 1rem; font-family: Lexend,sans-serif; } -.c3 { - width: 100%; +.c4 { min-height: 4rem; padding: 0.25rem; background: #f2f2f2; - border-radius: 0.375rem; + border-radius: 0.5rem; font-family: Lexend,sans-serif; } -.c5 { +.c7 { + width: 100%; + font-family: Lexend,sans-serif; +} + +.c8 { min-height: 3.5rem; padding-top: 1.25rem; padding-bottom: 1.25rem; padding-left: 0.75rem; padding-right: 1rem; + color: #222222; background: #ffffff; - border-bottom: 0.25rem solid; - border-color: transparent; border-radius: 0.5rem; font-family: Lexend,sans-serif; } -.c11 { +.c14 { font-family: Lexend,sans-serif; font-weight: 700; font-size: 1.125rem; @@ -219,7 +226,28 @@ exports[`OakQuizOrder matches snapshot 1`] = ` gap: 1rem; } -.c7 { +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 1rem; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-basis: 100%; + -ms-flex-preferred-size: 100%; + flex-basis: 100%; +} + +.c10 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -231,65 +259,87 @@ exports[`OakQuizOrder matches snapshot 1`] = ` gap: 1rem; } -.c12 { +.c15 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } -.c10 { +.c13 { object-fit: contain; } -.c6 { +.c9 { cursor: -webkit-grab; cursor: -moz-grab; cursor: grab; outline: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } -.c6:focus-visible:not([data-dragging="true"]):not([data-disabled="true"]) { +.c9:focus-visible:not([data-dragging="true"]):not([data-disabled="true"]) { box-shadow: 0 0 0 0.125rem rgba(255,229,85,100%), 0 0 0 0.3rem rgba(87,87,87,100%); } -.c6[data-dragging="true"] { +.c9[data-dragging="true"] { cursor: move; background-color: #bef2bd; - border: 0.25rem solid #222222; + color: #222222; + outline: 0.25rem solid #222222; + outline-offset: -0.25rem; box-shadow: 0.125rem 0.125rem 0 rgba(255,229,85,100%), 0.25rem 0.25rem 0 rgba(87,87,87,100%); + -webkit-text-decoration: underline; + text-decoration: underline; } -.c6[data-disabled="true"] { +.c9[data-disabled="true"] { cursor: default; background-color: #f2f2f2; color: #808080; } -.c6[data-readonly="true"] { +.c9[data-readonly="true"] { cursor: default; } -.c8 { +.c11 { margin-block: -0.5rem; } -.c4 { +.c6 { outline: 0.188rem solid #222222; outline-style: dashed; } -@media (hover:hover) { - .c6:hover:not([data-dragging="true"]):not([data-disabled="true"]):not( [data-readonly="true"] ) { - background-color: #dff9de; - box-shadow: 0 0.5rem 0.5rem rgba(92,92,92,20%); - border-bottom: 0.25rem solid #222222; +@media (min-width:750px) { + .c3 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } +} + +@media (min-width:1280px) { + .c3 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; } } @media (hover:hover) { - .c4:hover { - background-color: #ffffff; + .c9:hover:not([data-dragging="true"]):not([data-disabled="true"]):not( [data-readonly="true"] ) { + background-color: #dff9de; + color: #222222; + box-shadow: 0 0.5rem 0.5rem rgba(92,92,92,20%); + border-bottom: 0.25rem solid #222222; + padding-bottom: 1rem; + -webkit-text-decoration: underline; + text-decoration: underline; } } @@ -299,199 +349,215 @@ exports[`OakQuizOrder matches snapshot 1`] = ` role="listbox" >
- + -
-
- Mouse + /> +
+
+ Mouse +
- + -
-
- Hawk + /> +
+
+ Hawk +
- + -
-
- Grasshopper + /> +
+
+ Grasshopper +
, - @media (hover:hover) { + @media (min-width:750px) { + +} + +@media (min-width:1280px) { } @@ -513,7 +579,11 @@ exports[`OakQuizOrder matches snapshot 1`] = ` Press space again to drop the item in its new position, or press escape to cancel. , - @media (hover:hover) { + @media (min-width:750px) { + +} + +@media (min-width:1280px) { } From b99bed7efa2f5068f20af0933c03eb1d32725db6 Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Fri, 8 Mar 2024 09:56:29 +0000 Subject: [PATCH 07/15] feat(PUPIL-442): add a disabled state to OakDroppable --- .../OakDroppable/OakDroppable.stories.tsx | 12 ++++++++++ .../molecules/OakDroppable/OakDroppable.tsx | 24 +++++++++++++++++-- .../__snapshots__/OakDroppable.test.tsx.snap | 4 ++++ .../pupil/OakQuizOrder/OakQuizOrder.tsx | 10 ++++---- .../__snapshots__/OakQuizOrder.test.tsx.snap | 4 ++++ 5 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/components/molecules/OakDroppable/OakDroppable.stories.tsx b/src/components/molecules/OakDroppable/OakDroppable.stories.tsx index 5ca03052..4b0aba67 100644 --- a/src/components/molecules/OakDroppable/OakDroppable.stories.tsx +++ b/src/components/molecules/OakDroppable/OakDroppable.stories.tsx @@ -30,6 +30,12 @@ type Story = StoryObj; export const Default: Story = {}; +export const Disabled: Story = { + args: { + isDisabled: true, + }, +}; + /** * A draggable has entered the droppable so it has entered an active state */ @@ -41,18 +47,21 @@ export const DraggingOver: Story = { export const Occupied: Story = { args: { + canDrop: true, children: Elephant, }, }; export const WithSlotLabel: Story = { args: { + canDrop: true, labelSlot: "never forgets", }, }; export const OccupiedWithSlotLabel: Story = { args: { + canDrop: true, labelSlot: "never forgets", children: Elephant, }, @@ -62,6 +71,7 @@ export const OccupiedWithSlotLabel: Story = { */ export const DraggingOverWithSlotLabel: Story = { args: { + canDrop: true, isOver: true, labelSlot: "never forgets", }, @@ -69,6 +79,7 @@ export const DraggingOverWithSlotLabel: Story = { export const WithLongSlotLabel: Story = { args: { + canDrop: true, labelSlot: "which animal never forgets and is the largest land animal on earth?", }, @@ -76,6 +87,7 @@ export const WithLongSlotLabel: Story = { export const WithAVeryLongSlotLabel: Story = { args: { + canDrop: true, labelSlot: "which animal is the largest land mammal with a long trunk, large ears, and tusks? Known for intelligence and social behavior, it symbolizes strength and conservation efforts worldwide.", }, diff --git a/src/components/molecules/OakDroppable/OakDroppable.tsx b/src/components/molecules/OakDroppable/OakDroppable.tsx index 8f2ccd22..09b8a405 100644 --- a/src/components/molecules/OakDroppable/OakDroppable.tsx +++ b/src/components/molecules/OakDroppable/OakDroppable.tsx @@ -16,6 +16,10 @@ export type OakDroppableProps = { * Indicates whether a draggable is currently being dragged over the droppable */ isOver?: boolean; + /** + * Present the element in a state making it clear that it can be dropped into + */ + isDisabled?: boolean; /** * A slot for a label to be displayed to the RHS of the droppable * @@ -31,6 +35,10 @@ export type OakDroppableProps = { const StyledFlex = styled(OakFlex)` outline: ${parseBorder("border-solid-l")} ${parseColor("border-primary")}; outline-style: dashed; + + &[data-disabled="true"] { + outline-color: ${parseColor("border-neutral")}; + } `; /** @@ -44,7 +52,18 @@ export const OakDroppable: FC< > = forwardRef< HTMLDivElement, OakDroppableProps & ComponentPropsWithoutRef ->(({ children, labelSlot, isOver, ...props }, ref) => { +>(({ children, labelSlot, isOver, isDisabled, ...props }, ref) => { + const slotBackground = (() => { + if (isOver) { + return "bg-primary"; + } + if (!isDisabled) { + return "bg-neutral"; + } + + return "transparent"; + })(); + return ( {children} diff --git a/src/components/molecules/OakDroppable/__snapshots__/OakDroppable.test.tsx.snap b/src/components/molecules/OakDroppable/__snapshots__/OakDroppable.test.tsx.snap index b5db440f..7d002c30 100644 --- a/src/components/molecules/OakDroppable/__snapshots__/OakDroppable.test.tsx.snap +++ b/src/components/molecules/OakDroppable/__snapshots__/OakDroppable.test.tsx.snap @@ -47,6 +47,10 @@ exports[`OakDroppable matches snapshot 1`] = ` outline-style: dashed; } +.c4[data-disabled="true"] { + outline-color: #808080; +} + @media (min-width:750px) { .c1 { -webkit-flex-direction: row; diff --git a/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx index f6e66f9c..fffb6edd 100644 --- a/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx +++ b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx @@ -56,22 +56,20 @@ const ConnectedDraggable = ({ id, label }: OakQuizOrderItem) => { setNodeRef, transform, transition, - active, - over, + isOver, + isDragging, } = useSortable({ id }); - const isGhostItem = active?.id === id; - const isGhostSlot = over?.id === id; const style = { transform: CSS.Transform.toString(transform), transition, }; return ( - + Date: Fri, 8 Mar 2024 11:33:44 +0000 Subject: [PATCH 08/15] chore(PUPIL-442): add InternalDroppableHoldingPen this is used in `OakQuizMatch` to hold the items that are to be matched --- .../InternalDroppableHoldingPen.stories.tsx | 47 +++++++++++++++ .../InternalDroppableHoldingPen.test.tsx | 19 ++++++ .../InternalDroppableHoldingPen.tsx | 58 +++++++++++++++++++ .../InternalDroppableHoldingPen.test.tsx.snap | 36 ++++++++++++ .../InternalDroppableHoldingPen/index.ts | 1 + 5 files changed, 161 insertions(+) create mode 100644 src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.stories.tsx create mode 100644 src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.test.tsx create mode 100644 src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx create mode 100644 src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap create mode 100644 src/components/organisms/pupil/InternalDroppableHoldingPen/index.ts diff --git a/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.stories.tsx b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.stories.tsx new file mode 100644 index 00000000..117eddde --- /dev/null +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.stories.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; + +import { InternalDroppableHoldingPen } from "./InternalDroppableHoldingPen"; + +import { OakDraggable } from "@/components/molecules"; + +const meta: Meta = { + component: InternalDroppableHoldingPen, + tags: ["autodocs"], + title: "components/organisms/pupil/InternalDroppableHoldingPen", + parameters: { + controls: { + include: [], + }, + backgrounds: { + default: "light", + }, + }, + render: (args) => , +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +/** + * A draggable has entered the droppable so it has entered an active state + */ +export const DraggingOver: Story = { + args: { + isOver: true, + }, +}; + +export const Occupied: Story = { + args: { + children: ( + <> + Elephant + Kangaroo + Shark + + ), + }, +}; diff --git a/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.test.tsx b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.test.tsx new file mode 100644 index 00000000..8ab2567e --- /dev/null +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.test.tsx @@ -0,0 +1,19 @@ +import { ThemeProvider } from "styled-components"; +import { create } from "react-test-renderer"; +import React from "react"; + +import { InternalDroppableHoldingPen } from "./InternalDroppableHoldingPen"; + +import { oakDefaultTheme } from "@/styles"; + +describe("InternalDroppableHoldingPen", () => { + it("matches snapshot", () => { + const tree = create( + + + , + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx new file mode 100644 index 00000000..3f9c735f --- /dev/null +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx @@ -0,0 +1,58 @@ +import React, { + ComponentPropsWithRef, + ComponentPropsWithoutRef, + FC, + forwardRef, +} from "react"; +import styled from "styled-components"; + +import { OakFlex } from "@/components/atoms"; +import { parseColor } from "@/styles/helpers/parseColor"; + +const StyledOakFlex = styled(OakFlex)` + background-color: ${parseColor("grey20")}; + background-color: color-mix(in lch, ${parseColor("black")} 5%, transparent); + + &[data-over="true"] { + background-color: ${parseColor("white")}; + background-color: color-mix( + in lch, + ${parseColor("white")} 60%, + transparent + ); + } +`; + +type InternalDroppableHoldingPenProps = { + /** + * Indicates whether a draggable is currently being dragged over the droppable + */ + isOver?: boolean; +}; + +/** + * An internal holding pen for multiple draggable items + * + * Has no intrinsic drop functionally. + * It is intended to be used with `useDroppable` from `@dnd-kit/core` + */ +export const InternalDroppableHoldingPen: FC< + ComponentPropsWithRef +> = forwardRef< + HTMLDivElement, + InternalDroppableHoldingPenProps & ComponentPropsWithoutRef +>(({ isOver, ...props }, ref) => { + return ( + + ); +}); diff --git a/src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap b/src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap new file mode 100644 index 00000000..7c635e92 --- /dev/null +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InternalDroppableHoldingPen matches snapshot 1`] = ` +.c0 { + min-height: 5rem; + padding: 0.75rem; + margin-bottom: 2rem; + border-radius: 1rem; + font-family: Lexend,sans-serif; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + gap: 0.75rem; +} + +.c2 { + background-color: #f2f2f2; + background-color: color-mix(in lch,#222222 5%,transparent); +} + +.c2[data-over="true"] { + background-color: #ffffff; + background-color: color-mix( in lch, #ffffff 60%, transparent ); +} + +
+`; diff --git a/src/components/organisms/pupil/InternalDroppableHoldingPen/index.ts b/src/components/organisms/pupil/InternalDroppableHoldingPen/index.ts new file mode 100644 index 00000000..e1adbf0a --- /dev/null +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/index.ts @@ -0,0 +1 @@ +export * from "./InternalDroppableHoldingPen"; From d439f337f14390d2129f0585cbf8e888872f2070 Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Fri, 8 Mar 2024 11:38:40 +0000 Subject: [PATCH 09/15] feat(PUPIL-442): add `OakQuizMatch` --- .../molecules/OakDroppable/OakDroppable.tsx | 1 + .../OakQuizMatch/OakQuizMatch.stories.tsx | 76 ++ .../pupil/OakQuizMatch/OakQuizMatch.test.tsx | 169 ++++ .../pupil/OakQuizMatch/OakQuizMatch.tsx | 260 +++++++ .../__snapshots__/OakQuizMatch.test.tsx.snap | 728 ++++++++++++++++++ .../organisms/pupil/OakQuizMatch/index.ts | 1 + src/components/organisms/pupil/index.ts | 1 + 7 files changed, 1236 insertions(+) create mode 100644 src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.stories.tsx create mode 100644 src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx create mode 100644 src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx create mode 100644 src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap create mode 100644 src/components/organisms/pupil/OakQuizMatch/index.ts diff --git a/src/components/molecules/OakDroppable/OakDroppable.tsx b/src/components/molecules/OakDroppable/OakDroppable.tsx index 09b8a405..a513b62b 100644 --- a/src/components/molecules/OakDroppable/OakDroppable.tsx +++ b/src/components/molecules/OakDroppable/OakDroppable.tsx @@ -96,6 +96,7 @@ export const OakDroppable: FC< $flexBasis="100%" $width="100%" $alignSelf="center" + data-testid="label" > {labelSlot} diff --git a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.stories.tsx b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.stories.tsx new file mode 100644 index 00000000..5e314cbc --- /dev/null +++ b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.stories.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; + +import { OakQuizMatch } from "./OakQuizMatch"; + +const meta: Meta = { + component: OakQuizMatch, + tags: ["autodocs"], + title: "components/organisms/pupil/OakQuizMatch", + parameters: { + controls: { + include: [], + }, + backgrounds: { + default: "light", + }, + }, + render: (args) => , +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + initialOptions: [ + { id: "1", label: "Comma" }, + { id: "2", label: "Apostrophe" }, + { id: "3", label: "Question mark" }, + { id: "4", label: "Full stop" }, + { id: "5", label: "Exclamation mark" }, + ], + initialSlots: [ + { id: "1", label: "conveys intense emotion" }, + { id: "2", label: "poses a question" }, + { id: "3", label: "ends a declarative sentence" }, + { id: "4", label: "separates a main clause and a subordinate clause" }, + { id: "5", label: "shows belonging" }, + ], + }, +}; + +export const WithManyOptions: Story = { + args: { + initialOptions: [ + { id: "1", label: "Book" }, + { id: "2", label: "Bicycle" }, + { id: "3", label: "Guitar" }, + { id: "4", label: "Camera" }, + { id: "5", label: "Paintbrush" }, + { id: "6", label: "Football" }, + { id: "7", label: "Cooking pot" }, + { id: "8", label: "Telescope" }, + { id: "9", label: "Headphones" }, + { id: "10", label: "Passport" }, + ], + initialSlots: [ + { id: "1", label: "a source of knowledge and adventure" }, + { id: "2", label: "a mode of transportation and exercise" }, + { + id: "3", + label: "a musical instrument for creating melodies and rhythms", + }, + { id: "4", label: "captures memories and moments in time" }, + { id: "5", label: "used to express creativity on canvas" }, + { id: "6", label: "a tool for teamwork and athletic skill" }, + { id: "7", label: "essential for preparing delicious meals" }, + { id: "8", label: "explores the wonders of the cosmos" }, + { id: "9", label: "provides immersive audio experiences" }, + { + id: "10", + label: "gateway to new destinations and cultural experiences", + }, + ], + }, +}; diff --git a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx new file mode 100644 index 00000000..17b66c61 --- /dev/null +++ b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx @@ -0,0 +1,169 @@ +import assert from "assert"; + +import { act, create } from "react-test-renderer"; +import { ThemeProvider } from "styled-components"; +import React from "react"; +import { DndContext, DndContextProps, UniqueIdentifier } from "@dnd-kit/core"; +import { + getAllByRole as getAllByRoleWithin, + getByTestId as getByTestIdWithin, + getByRole as getByRoleWithin, + queryByRole as queryByRoleWithin, +} from "@testing-library/react"; + +import { OakQuizMatch } from "./OakQuizMatch"; + +import { oakDefaultTheme } from "@/styles"; +import renderWithTheme from "@/test-helpers/renderWithTheme"; +import { injectDndContext } from "@/components/atoms/InternalDndContext/InternalDndContext"; + +window.matchMedia = jest.fn().mockReturnValue({ + matches: false, +}); + +const options = [ + { id: "1", label: "Exclamation mark" }, + { id: "2", label: "Full stop" }, + { id: "3", label: "Question mark" }, +]; +const slots = [ + { id: "1", label: "conveys intense emotion" }, + { id: "2", label: "poses a question" }, + { id: "3", label: "ends a declarative sentence" }, +]; + +describe(OakQuizMatch, () => { + it("matches snapshot", () => { + const tree = create( + + + , + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it("allows an option to be dropped into a slot", async () => { + let dndProps: DndContextProps = {}; + const MockDndContext = (props: DndContextProps) => { + dndProps = props; + return ; + }; + const onChangeSpy = jest.fn(); + + const { getByTestId, getAllByTestId } = renderWithTheme( + + + , + ); + const [firstSlot] = getAllByTestId("slot"); + assert(firstSlot); + + // All items should start in the holding pen + expect( + getAllByRoleWithin(getByTestId("holding-pen"), "option").map( + (item) => item.textContent, + ), + ).toEqual(["Exclamation mark", "Full stop", "Question mark"]); + + // Drag the first item into the first slot + act(() => { + dndProps?.onDragStart?.(mockDragEvent("1")); + dndProps?.onDragOver?.(mockDragEvent("1", "1")); + dndProps?.onDragEnd?.(mockDragEvent("1", "1")); + }); + + // The first item should be removed from the holding pen and placed in the first slot + expect( + getAllByRoleWithin(getByTestId("holding-pen"), "option").map( + (item) => item.textContent, + ), + ).toEqual(["Full stop", "Question mark"]); + expect(getByTestIdWithin(firstSlot, "label").textContent).toEqual( + "conveys intense emotion", + ); + expect(getByRoleWithin(firstSlot, "option").textContent).toEqual( + "Exclamation mark", + ); + + // Replace the first item + act(() => { + dndProps?.onDragStart?.(mockDragEvent("2")); + dndProps?.onDragOver?.(mockDragEvent("2", "1")); + dndProps?.onDragEnd?.(mockDragEvent("2", "1")); + }); + + // The first option should be returned to the holding pen + expect( + getAllByRoleWithin(getByTestId("holding-pen"), "option").map( + (item) => item.textContent, + ), + ).toEqual(["Exclamation mark", "Question mark"]); + // The first slot should now contain the second option + expect(getByRoleWithin(firstSlot, "option").textContent).toEqual( + "Full stop", + ); + + // Drag the option back into the holding pen + act(() => { + dndProps?.onDragStart?.(mockDragEvent("2")); + dndProps?.onDragOver?.(mockDragEvent("2", "holding-pen")); + dndProps?.onDragEnd?.(mockDragEvent("2", "holding-pen")); + }); + + // All options should be back in the holding pen + expect( + getAllByRoleWithin(getByTestId("holding-pen"), "option").map( + (item) => item.textContent, + ), + ).toEqual(["Exclamation mark", "Full stop", "Question mark"]); + expect(getByTestIdWithin(firstSlot, "label").textContent).toEqual( + "conveys intense emotion", + ); + expect(queryByRoleWithin(firstSlot, "option")).toBeNull(); + }); +}); + +function mockDragEvent(optionId: UniqueIdentifier, slotId?: UniqueIdentifier) { + return { + active: { + id: optionId, + data: { + current: options.find((option) => option.id === optionId), + }, + rect: { + current: { + initial: null, + translated: null, + }, + }, + }, + over: slotId + ? { + id: slotId, + rect: { + width: 0, + height: 0, + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + disabled: false, + data: { + current: slots.find((slot) => slot.id === slotId), + }, + } + : null, + activatorEvent: new Event("mock-pointer"), + collisions: null, + delta: { + x: 0, + y: 0, + }, + }; +} diff --git a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx new file mode 100644 index 00000000..2be2d293 --- /dev/null +++ b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx @@ -0,0 +1,260 @@ +import React, { ReactNode, useRef, useState } from "react"; +import { + closestCenter, + KeyboardSensor, + useSensor, + useSensors, + DragEndEvent, + DragOverlay, + DragStartEvent, + Announcements, + MouseSensor, + TouchSensor, + useDraggable, + useDroppable, +} from "@dnd-kit/core"; +import { createPortal } from "react-dom"; + +import { InternalDroppableHoldingPen } from "../InternalDroppableHoldingPen"; + +import { OakFlex } from "@/components/atoms"; +import { + OakDragAndDropInstructions, + OakDraggable, + OakDroppable, +} from "@/components/molecules"; +import { usePrefersReducedMotion } from "@/animation/usePrefersReducedMotion"; +import { InternalDndContext } from "@/components/atoms/InternalDndContext/InternalDndContext"; + +type DraggableId = string; +type DroppableId = string; +type DraggableItem = { + id: DraggableId; + label: string; +}; +type DroppableItem = { + id: DroppableId; + label: string; +}; +type Matches = Record; + +export type OakQuizMatchProps = { + /** + * The initial options + * + * these are the items that can be dragged into a slot to form a match + * + * this cannot be updated on subsequent renders + */ + initialOptions: DraggableItem[]; + /** + * The initial slots + * + * these are the slots into which an option can be dropped to form a match + * + * this cannot be updated on subsequent renders + */ + initialSlots: DroppableItem[]; + /** + * Notify the consumer when matches have changed + */ + onChange?: (matches: Matches) => void; +}; + +const ConnectedDraggable = ({ + id, + label, + isOver, +}: DraggableItem & { isOver?: boolean }) => { + const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ + id, + data: { id, label }, + }); + + return ( + + {label} + + ); +}; + +const ConnectedDroppableHoldingPen = ({ + children, +}: { + children?: ReactNode; +}) => { + const { setNodeRef, isOver } = useDroppable({ + id: "holding-pen", + data: { label: "holding pen" }, + }); + + return ( + + {children} + + ); +}; + +const ConnectedDroppable = ({ + id, + label, + match, +}: DroppableItem & { match?: DraggableItem }) => { + const { setNodeRef, isOver, active } = useDroppable({ + id, + data: { id, label }, + }); + + return ( + + {match && } + + ); +}; + +/** + * A list of draggable items with matching slots to drop them into. + * + * Keyboard navigation is supported with the `tab`, `space` and `arrow` keys + */ +export const OakQuizMatch = ({ + initialOptions, + initialSlots, + onChange, +}: OakQuizMatchProps) => { + const [matches, setMatches] = useState({}); + const draggables = useRef(initialOptions).current; + const droppables = useRef(initialSlots).current; + const [activeId, setActiveId] = useState(null); + const activeDraggable = draggables.find((item) => item.id === activeId); + const prefersReducedMotion = usePrefersReducedMotion(); + const sensors = useSensors( + useSensor(MouseSensor), + useSensor(TouchSensor), + useSensor(KeyboardSensor, { + scrollBehavior: prefersReducedMotion ? "instant" : "smooth", + }), + ); + const matchedDraggableIds = Object.values(matches).map((item) => item.id); + const unmatchedDraggables = draggables.filter( + (draggable) => !matchedDraggableIds.includes(draggable.id), + ); + return ( + <> + + + + {unmatchedDraggables.map((item) => ( + + ))} + + + {droppables.map((droppable) => ( + + ))} + + {createPortal( + + {activeDraggable && ( + {activeDraggable.label} + )} + , + document.body, + )} + + + ); + + function handleDragStart(event: DragStartEvent) { + const { active } = event; + setActiveId(active.id.toString()); + } + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + + if (over) { + setMatches((matches) => { + // Remove the draggable from its current slot + const entries = Object.entries(matches).filter( + ([, draggable]) => draggable?.id !== active.id, + ); + const newMatches = Object.fromEntries(entries); + + if (over.id !== "holding-pen") { + // We've dropped the draggable into a slot so add it to the new slot + newMatches[over.id] = active.data.current as DraggableItem; + } + + onChange?.(newMatches); + + return newMatches; + }); + } + + setActiveId(null); + } +}; + +const announcements: Announcements = { + onDragStart() { + return undefined; + }, + onDragOver({ active, over }) { + if (over?.data.current && active.data?.current) { + return `Item ${active.data.current.label} was moved over ${over.data.current.label}`; + } + }, + onDragEnd({ active, over }) { + if (over?.data.current && active.data?.current) { + return `Item ${active.data.current.label} was dropped onto ${over.data.current.label}`; + } + }, + onDragCancel({ active }) { + if (active.data?.current) { + return `Dragging was cancelled. Item ${active.data.current.label} was dropped.`; + } + }, +}; diff --git a/src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap b/src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap new file mode 100644 index 00000000..5358bca8 --- /dev/null +++ b/src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap @@ -0,0 +1,728 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OakQuizMatch matches snapshot 1`] = ` +[ + .c0 { + margin-bottom: 2rem; + font-family: Lexend,sans-serif; +} + +.c2 { + font-family: Lexend,sans-serif; +} + +.c4 { + position: relative; + width: 2rem; + height: 2rem; + font-family: Lexend,sans-serif; +} + +.c6 { + font-family: Lexend,sans-serif; + font-weight: 300; + font-size: 1rem; + line-height: 1.5rem; + -webkit-letter-spacing: -0.005rem; + -moz-letter-spacing: -0.005rem; + -ms-letter-spacing: -0.005rem; + letter-spacing: -0.005rem; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 0.5rem; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c8 { + background: #ffffff; + padding-left: 0.5rem; + padding-right: 0.5rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + border: 0.125rem solid; + border-color: #7c9aec; + border-radius: 0.375rem; + font-family: Lexend,sans-serif; + font-weight: 700; + font-size: 0.875rem; + line-height: 1.25rem; + -webkit-letter-spacing: -0.005rem; + -moz-letter-spacing: -0.005rem; + -ms-letter-spacing: -0.005rem; + letter-spacing: -0.005rem; + white-space: nowrap; +} + +.c5 { + object-fit: contain; +} + +.c7 { + margin-block: calc(-0.5rem / 2); + line-height: calc(1.5rem + 0.5rem); +} + +@media (min-width:750px) { + +} + +@media (min-width:1280px) { + +} + +@media (hover:hover) { + +} + +
+
+
+ move-arrows +
+
+
+ Click and drag answers to change the order, or select using + + + + Tab + + + then move by pressing + + Space + + and the + + ↑ + + + + ↓ + + arrows on your keyboard. +
+
, + .c5 { + font-family: Lexend,sans-serif; +} + +.c8 { + position: relative; + width: 2rem; + height: 2rem; + font-family: Lexend,sans-serif; +} + +.c0 { + min-height: 5rem; + padding: 0.75rem; + margin-bottom: 2rem; + border-radius: 1rem; + font-family: Lexend,sans-serif; +} + +.c3 { + min-height: 3.5rem; + padding-top: 1.25rem; + padding-bottom: 1.25rem; + padding-left: 0.75rem; + padding-right: 1rem; + color: #ffffff; + background: #222222; + border-radius: 0.5rem; + font-family: Lexend,sans-serif; +} + +.c10 { + font-family: Lexend,sans-serif; + font-weight: 700; + font-size: 1.125rem; + line-height: 1.75rem; + -webkit-letter-spacing: -0.005rem; + -moz-letter-spacing: -0.005rem; + -ms-letter-spacing: -0.005rem; + letter-spacing: -0.005rem; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + gap: 0.75rem; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: 1rem; +} + +.c11 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c9 { + object-fit: contain; +} + +.c2 { + background-color: #f2f2f2; + background-color: color-mix(in lch,#222222 5%,transparent); +} + +.c2[data-over="true"] { + background-color: #ffffff; + background-color: color-mix( in lch, #ffffff 60%, transparent ); +} + +.c4 { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + outline: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c4:focus-visible:not([data-dragging="true"]):not([data-disabled="true"]) { + box-shadow: 0 0 0 0.125rem rgba(255,229,85,100%), 0 0 0 0.3rem rgba(87,87,87,100%); +} + +.c4[data-dragging="true"] { + cursor: move; + background-color: #bef2bd; + color: #222222; + outline: 0.25rem solid #222222; + outline-offset: -0.25rem; + box-shadow: 0.125rem 0.125rem 0 rgba(255,229,85,100%), 0.25rem 0.25rem 0 rgba(87,87,87,100%); + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c4[data-disabled="true"] { + cursor: default; + background-color: #f2f2f2; + color: #808080; +} + +.c4[data-readonly="true"] { + cursor: default; +} + +.c7 { + margin-block: -0.5rem; +} + +@media (min-width:750px) { + +} + +@media (min-width:1280px) { + +} + +@media (hover:hover) { + .c4:hover:not([data-dragging="true"]):not([data-disabled="true"]):not( [data-readonly="true"] ) { + background-color: #dff9de; + color: #222222; + box-shadow: 0 0.5rem 0.5rem rgba(92,92,92,20%); + border-bottom: 0.25rem solid #222222; + padding-bottom: 1rem; + -webkit-text-decoration: underline; + text-decoration: underline; + } +} + +
+
+
+
+ +
+
+ Exclamation mark +
+
+
+
+
+
+ +
+
+ Full stop +
+
+
+
+
+
+ +
+
+ Question mark +
+
+
+
, + .c0 { + font-family: Lexend,sans-serif; +} + +.c2 { + padding: 1rem; + background: #cee7e5; + border-radius: 1rem; + font-family: Lexend,sans-serif; +} + +.c4 { + min-height: 4rem; + padding: 0.25rem; + background: transparent; + border-radius: 0.5rem; + font-family: Lexend,sans-serif; +} + +.c7 { + width: 100%; + font-family: Lexend,sans-serif; +} + +.c8 { + width: 100%; + min-height: 3.5rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + background: #e7f6f5; + border-radius: 0.5rem; + font-family: Lexend,sans-serif; + font-weight: 300; + font-size: 1.125rem; + line-height: 1.75rem; + -webkit-letter-spacing: -0.005rem; + -moz-letter-spacing: -0.005rem; + -ms-letter-spacing: -0.005rem; + letter-spacing: -0.005rem; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 1rem; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 1rem; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-basis: 100%; + -ms-flex-preferred-size: 100%; + flex-basis: 100%; +} + +.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + -webkit-flex-basis: 100%; + -ms-flex-preferred-size: 100%; + flex-basis: 100%; +} + +.c6 { + outline: 0.188rem solid #222222; + outline-style: dashed; +} + +.c6[data-disabled="true"] { + outline-color: #808080; +} + +@media (min-width:750px) { + .c3 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } +} + +@media (min-width:1280px) { + .c3 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } +} + +@media (hover:hover) { + +} + +
+
+
+
+
+
+ conveys intense emotion +
+
+
+
+
+
+
+ poses a question +
+
+
+
+
+
+
+ ends a declarative sentence +
+
+
, + @media (min-width:750px) { + +} + +@media (min-width:1280px) { + +} + +@media (hover:hover) { + +} + +
+ + To pick up a draggable item, press the space bar. + While dragging, use the arrow keys to move the item. + Press space again to drop the item in its new position, or press escape to cancel. + +
, + @media (min-width:750px) { + +} + +@media (min-width:1280px) { + +} + +@media (hover:hover) { + +} + +
, +] +`; diff --git a/src/components/organisms/pupil/OakQuizMatch/index.ts b/src/components/organisms/pupil/OakQuizMatch/index.ts new file mode 100644 index 00000000..8338133b --- /dev/null +++ b/src/components/organisms/pupil/OakQuizMatch/index.ts @@ -0,0 +1 @@ +export * from "./OakQuizMatch"; diff --git a/src/components/organisms/pupil/index.ts b/src/components/organisms/pupil/index.ts index caa869f3..a81e4d3d 100644 --- a/src/components/organisms/pupil/index.ts +++ b/src/components/organisms/pupil/index.ts @@ -12,3 +12,4 @@ export * from "./OakLessonNavItem"; export * from "./OakLessonReviewItem"; export * from "./OakLessonVideoTranscript"; export * from "./OakQuizOrder"; +export * from "./OakQuizMatch"; From 6347faee8160cd55d3cf0b93fc9e099835ddbe02 Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Fri, 8 Mar 2024 15:28:18 +0000 Subject: [PATCH 10/15] feat(PUPIL-442): prevent the holding pen from shrinking when an item is removed --- .../InternalDroppableHoldingPen.tsx | 60 +++- .../InternalDroppableHoldingPen.test.tsx.snap | 31 +- .../pupil/OakQuizMatch/OakQuizMatch.test.tsx | 17 +- .../__snapshots__/OakQuizMatch.test.tsx.snap | 333 +++++++++--------- .../pupil/OakQuizOrder/OakQuizOrder.test.tsx | 8 +- 5 files changed, 269 insertions(+), 180 deletions(-) diff --git a/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx index 3f9c735f..4407cc34 100644 --- a/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx @@ -3,13 +3,15 @@ import React, { ComponentPropsWithoutRef, FC, forwardRef, + useLayoutEffect, + useState, } from "react"; import styled from "styled-components"; -import { OakFlex } from "@/components/atoms"; +import { OakBox, OakFlex } from "@/components/atoms"; import { parseColor } from "@/styles/helpers/parseColor"; -const StyledOakFlex = styled(OakFlex)` +const StyledOakBox = styled(OakBox)` background-color: ${parseColor("grey20")}; background-color: color-mix(in lch, ${parseColor("black")} 5%, transparent); @@ -41,18 +43,58 @@ export const InternalDroppableHoldingPen: FC< > = forwardRef< HTMLDivElement, InternalDroppableHoldingPenProps & ComponentPropsWithoutRef ->(({ isOver, ...props }, ref) => { +>(({ isOver, children, ...props }, ref) => { + const [domContent, setContentBox] = useState(null); + const [minHeight, setMinHeight] = useState(0); + + useLayoutEffect(() => { + if (!domContent) { + return; + } + + // Prevents the holding area from shrinking when an item is removed + // avoiding layout shift + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setMinHeight((currentHeight) => + Math.max(entry.borderBoxSize[0]?.blockSize ?? 0, currentHeight), + ); + } + }); + + observer.observe(domContent); + + // Reset min height when the window is resized so that the holding pen can shrink + function resetMinHeight() { + setMinHeight(0); + } + window.addEventListener("resize", resetMinHeight); + + return () => { + observer.disconnect(); + window.removeEventListener("resize", resetMinHeight); + }; + }, [domContent]); + return ( - + > + + {children} + + ); }); diff --git a/src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap b/src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap index 7c635e92..39c0bc65 100644 --- a/src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap @@ -2,14 +2,18 @@ exports[`InternalDroppableHoldingPen matches snapshot 1`] = ` .c0 { - min-height: 5rem; - padding: 0.75rem; margin-bottom: 2rem; border-radius: 1rem; font-family: Lexend,sans-serif; } -.c1 { +.c2 { + min-height: 5rem; + padding: 0.75rem; + font-family: Lexend,sans-serif; +} + +.c3 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -17,20 +21,33 @@ exports[`InternalDroppableHoldingPen matches snapshot 1`] = ` -webkit-flex-wrap: wrap; -ms-flex-wrap: wrap; flex-wrap: wrap; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; gap: 0.75rem; } -.c2 { +.c1 { background-color: #f2f2f2; background-color: color-mix(in lch,#222222 5%,transparent); } -.c2[data-over="true"] { +.c1[data-over="true"] { background-color: #ffffff; background-color: color-mix( in lch, #ffffff 60%, transparent ); }
+ className="c0 c1" + style={ + { + "minHeight": "auto", + } + } +> +
+
`; diff --git a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx index 17b66c61..e114c54a 100644 --- a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx +++ b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx @@ -17,9 +17,20 @@ import { oakDefaultTheme } from "@/styles"; import renderWithTheme from "@/test-helpers/renderWithTheme"; import { injectDndContext } from "@/components/atoms/InternalDndContext/InternalDndContext"; -window.matchMedia = jest.fn().mockReturnValue({ - matches: false, -}); +// Not currently implemented by JSDOM so we can provide stubs +window.matchMedia = + window.matchMedia ?? + jest.fn().mockReturnValue({ + matches: false, + }); + +class MockResizeObserver implements ResizeObserver { + disconnect() {} + observe() {} + unobserve() {} +} + +window.ResizeObserver = window.ResizeObserver ?? MockResizeObserver; const options = [ { id: "1", label: "Exclamation mark" }, diff --git a/src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap b/src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap index 5358bca8..219f1529 100644 --- a/src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap +++ b/src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap @@ -161,11 +161,11 @@ exports[`OakQuizMatch matches snapshot 1`] = ` arrows on your keyboard.
, - .c5 { + .c6 { font-family: Lexend,sans-serif; } -.c8 { +.c9 { position: relative; width: 2rem; height: 2rem; @@ -173,14 +173,18 @@ exports[`OakQuizMatch matches snapshot 1`] = ` } .c0 { - min-height: 5rem; - padding: 0.75rem; margin-bottom: 2rem; border-radius: 1rem; font-family: Lexend,sans-serif; } -.c3 { +.c2 { + min-height: 5rem; + padding: 0.75rem; + font-family: Lexend,sans-serif; +} + +.c4 { min-height: 3.5rem; padding-top: 1.25rem; padding-bottom: 1.25rem; @@ -192,7 +196,7 @@ exports[`OakQuizMatch matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c10 { +.c11 { font-family: Lexend,sans-serif; font-weight: 700; font-size: 1.125rem; @@ -203,7 +207,7 @@ exports[`OakQuizMatch matches snapshot 1`] = ` letter-spacing: -0.005rem; } -.c1 { +.c3 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -211,10 +215,14 @@ exports[`OakQuizMatch matches snapshot 1`] = ` -webkit-flex-wrap: wrap; -ms-flex-wrap: wrap; flex-wrap: wrap; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; gap: 0.75rem; } -.c6 { +.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -226,28 +234,28 @@ exports[`OakQuizMatch matches snapshot 1`] = ` gap: 1rem; } -.c11 { +.c12 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; } -.c9 { +.c10 { object-fit: contain; } -.c2 { +.c1 { background-color: #f2f2f2; background-color: color-mix(in lch,#222222 5%,transparent); } -.c2[data-over="true"] { +.c1[data-over="true"] { background-color: #ffffff; background-color: color-mix( in lch, #ffffff 60%, transparent ); } -.c4 { +.c5 { cursor: -webkit-grab; cursor: -moz-grab; cursor: grab; @@ -258,11 +266,11 @@ exports[`OakQuizMatch matches snapshot 1`] = ` user-select: none; } -.c4:focus-visible:not([data-dragging="true"]):not([data-disabled="true"]) { +.c5:focus-visible:not([data-dragging="true"]):not([data-disabled="true"]) { box-shadow: 0 0 0 0.125rem rgba(255,229,85,100%), 0 0 0 0.3rem rgba(87,87,87,100%); } -.c4[data-dragging="true"] { +.c5[data-dragging="true"] { cursor: move; background-color: #bef2bd; color: #222222; @@ -273,17 +281,17 @@ exports[`OakQuizMatch matches snapshot 1`] = ` text-decoration: underline; } -.c4[data-disabled="true"] { +.c5[data-disabled="true"] { cursor: default; background-color: #f2f2f2; color: #808080; } -.c4[data-readonly="true"] { +.c5[data-readonly="true"] { cursor: default; } -.c7 { +.c8 { margin-block: -0.5rem; } @@ -296,7 +304,7 @@ exports[`OakQuizMatch matches snapshot 1`] = ` } @media (hover:hover) { - .c4:hover:not([data-dragging="true"]):not([data-disabled="true"]):not( [data-readonly="true"] ) { + .c5:hover:not([data-dragging="true"]):not([data-disabled="true"]):not( [data-readonly="true"] ) { background-color: #dff9de; color: #222222; box-shadow: 0 0.5rem 0.5rem rgba(92,92,92,20%); @@ -308,170 +316,179 @@ exports[`OakQuizMatch matches snapshot 1`] = ` }
- + -
-
- Exclamation mark + /> +
+
+ Exclamation mark +
-
-
- + -
-
- Full stop + /> +
+
+ Full stop +
-
-
- + -
-
- Question mark + /> +
+
+ Question mark +
diff --git a/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx index f81f4d1b..b2fd4513 100644 --- a/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx +++ b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx @@ -9,9 +9,11 @@ import { oakDefaultTheme } from "@/styles"; import renderWithTheme from "@/test-helpers/renderWithTheme"; import { injectDndContext } from "@/components/atoms/InternalDndContext/InternalDndContext"; -window.matchMedia = jest.fn().mockReturnValue({ - matches: false, -}); +window.matchMedia = + window.matchMedia ?? + jest.fn().mockReturnValue({ + matches: false, + }); describe(OakQuizOrder, () => { const initialItems = [ From 946831ee9ba2d1954d5ba01db7da3522dbd57a9b Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Fri, 8 Mar 2024 16:24:40 +0000 Subject: [PATCH 11/15] feat(PUPIL-442): use the sortable keyboard coordinates to move match items so they snap to the nearest drop zone when hitting the arrow keys --- src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx index 2be2d293..799f214d 100644 --- a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx +++ b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx @@ -14,6 +14,7 @@ import { useDroppable, } from "@dnd-kit/core"; import { createPortal } from "react-dom"; +import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { InternalDroppableHoldingPen } from "../InternalDroppableHoldingPen"; @@ -159,6 +160,7 @@ export const OakQuizMatch = ({ useSensor(MouseSensor), useSensor(TouchSensor), useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, scrollBehavior: prefersReducedMotion ? "instant" : "smooth", }), ); From bbf3daf56bb8d36f1962bee73ef649c3fe93bd07 Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Fri, 8 Mar 2024 17:13:45 +0000 Subject: [PATCH 12/15] feat(PUPIL-442): tweak a11y roles and labels for match quiz component --- .../organisms/pupil/OakQuizMatch/OakQuizMatch.tsx | 10 +++++----- .../__snapshots__/OakQuizMatch.test.tsx.snap | 6 ++---- .../organisms/pupil/OakQuizOrder/OakQuizOrder.tsx | 9 ++------- 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx index 799f214d..9460bc81 100644 --- a/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx +++ b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx @@ -81,12 +81,12 @@ const ConnectedDraggable = ({ iconColor="icon-main" {...attributes} {...listeners} + role="option" aria-describedby={undefined} aria-roledescription="draggable item" aria-pressed={undefined} aria-selected={!!attributes["aria-pressed"]} style={{ opacity: isDragging ? 0 : 1 }} - role="option" > {label} @@ -107,8 +107,9 @@ const ConnectedDroppableHoldingPen = ({ {children} @@ -130,7 +131,6 @@ const ConnectedDroppable = ({ isOver={isOver} isDisabled={!active} ref={setNodeRef} - aria-roledescription="droppable slot" id={id} labelSlot={label} data-testid="slot" @@ -186,8 +186,8 @@ export const OakQuizMatch = ({ {droppables.map((droppable) => (
item.id === id) + 1; const getItemLabel = (id: UniqueIdentifier) => items.find((item) => item.id === id)?.label; - let firstAnnouncement = true; return { onDragStart() { return undefined; }, onDragOver({ active, over }) { - // Don't make an announcement for the first drag over since this is the initial position - if (over && !firstAnnouncement) { + if (over) { return `Sortable item ${getItemLabel( active.id, - )} was moved into position ${getPosition(over.id)} of ${items.length}`; + )} is in position ${getPosition(over.id)} of ${items.length}`; } - firstAnnouncement = false; }, onDragEnd({ active, over }) { - firstAnnouncement = true; if (over) { return `Sortable item ${getItemLabel( active.id, @@ -193,7 +189,6 @@ function createAccouncements(items: OakQuizOrderItem[]): Announcements { } }, onDragCancel({ active }) { - firstAnnouncement = true; return `Dragging was cancelled. Sortable item ${getItemLabel( active.id, )} was dropped.`; From 759fa9b0b9c91d10cdb2bf491c9c19f79b6cc6de Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Tue, 12 Mar 2024 14:33:01 +0000 Subject: [PATCH 13/15] fix(PUPIL-442): don't leave space answer feedback when it isn't present --- .../OakLessonBottomNav.stories.tsx | 21 +- .../OakLessonBottomNav/OakLessonBottomNav.tsx | 2 +- .../OakLessonBottomNav.test.tsx.snap | 474 ++++++++++-------- .../OakQuizFeedback.stories.tsx | 7 + .../pupil/OakQuizFeedback/OakQuizFeedback.tsx | 16 +- 5 files changed, 304 insertions(+), 216 deletions(-) diff --git a/src/components/organisms/pupil/OakLessonBottomNav/OakLessonBottomNav.stories.tsx b/src/components/organisms/pupil/OakLessonBottomNav/OakLessonBottomNav.stories.tsx index dffe573f..15b0be90 100644 --- a/src/components/organisms/pupil/OakLessonBottomNav/OakLessonBottomNav.stories.tsx +++ b/src/components/organisms/pupil/OakLessonBottomNav/OakLessonBottomNav.stories.tsx @@ -38,6 +38,7 @@ const meta: Meta = { default: "light", }, }, + render: (args) => , }; export default meta; @@ -117,7 +118,7 @@ export const WithFeedbackAndButton: Story = { export const WithLongFeedbackAndButton: Story = { render: (args) => ( - + ), args: { + feedback: "incorrect", answerFeedback: "Correct answer: George Orwell, a renowned British author, penned the dystopian masterpiece '1984' in 1949, depicting a totalitarian society under constant surveillance, influencing literature and pop culture for decades.", }, }; + +export const WithNoAnswerFeedbackAndButton: Story = { + render: (args) => ( + + + Next question + + + ), + args: { + feedback: "incorrect", + }, +}; diff --git a/src/components/organisms/pupil/OakLessonBottomNav/OakLessonBottomNav.tsx b/src/components/organisms/pupil/OakLessonBottomNav/OakLessonBottomNav.tsx index 5e78d3ff..cdd7d301 100644 --- a/src/components/organisms/pupil/OakLessonBottomNav/OakLessonBottomNav.tsx +++ b/src/components/organisms/pupil/OakLessonBottomNav/OakLessonBottomNav.tsx @@ -54,7 +54,7 @@ export const OakLessonBottomNav = ({ $minHeight="all-spacing-9" $gap="space-between-m" > - {content} + {content} :first-child:focus-visible .shadow { +.c8 > :first-child:focus-visible .shadow { box-shadow: 0 0 0 0.125rem rgba(255,229,85,100%), 0 0 0 0.3rem rgba(87,87,87,100%); } -.c6 > :first-child:hover .shadow { +.c8 > :first-child:hover .shadow { box-shadow: 0.125rem 0.125rem 0 rgba(255,229,85,100%); } -.c6 > :first-child:active .shadow { +.c8 > :first-child:active .shadow { box-shadow: 0.125rem 0.125rem 0 rgba(255,229,85,100%), 0.25rem 0.25rem 0 rgba(87,87,87,100%); } -.c6 > :first-child:disabled .icon-container { +.c8 > :first-child:disabled .icon-container { background: #808080; } -.c6 > :first-child:hover .icon-container { +.c8 > :first-child:hover .icon-container { background: #ffe555; } -.c6 > :first-child:active .icon-container { +.c8 > :first-child:active .icon-container { background: #ffe555; } -.c7:hover .shadow { +.c9:hover .shadow { box-shadow: none !important; } -.c7:active .shadow { +.c9:active .shadow { box-shadow: 0.125rem 0.125rem 0 rgba(255,229,85,100%), 0.25rem 0.25rem 0 rgba(87,87,87,100%) !important; } @@ -226,7 +237,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c18 { + .c19 { width: auto; } } @@ -240,7 +251,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c19 { + .c20 { -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; @@ -255,63 +266,67 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` className="c3 c4" >
- + +
, .c0 { @@ -324,7 +339,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c10 { +.c11 { width: 100%; height: -webkit-fit-content; height: -moz-fit-content; @@ -332,7 +347,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c5 { +.c6 { width: 1.5rem; height: 1.5rem; padding: 0rem; @@ -341,7 +356,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c6 { +.c7 { position: relative; width: 100%; height: 100%; @@ -359,7 +374,18 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` gap: 1.5rem; } -.c11 { +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c12 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -377,7 +403,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` flex-grow: 1; } -.c4 { +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -385,7 +411,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` gap: 0.75rem; } -.c8 { +.c9 { color: #287c34; font-family: Lexend,sans-serif; font-weight: 600; @@ -397,7 +423,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` letter-spacing: 0.0115rem; } -.c9 { +.c10 { margin-top: 0.75rem; font-family: Lexend,sans-serif; font-weight: 700; @@ -409,7 +435,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` letter-spacing: -0.005rem; } -.c7 { +.c8 { -webkit-filter: invert(98%) sepia(98%) saturate(0%) hue-rotate(328deg) brightness(102%) contrast(102%); filter: invert(98%) sepia(98%) saturate(0%) hue-rotate(328deg) brightness(102%) contrast(102%); object-fit: contain; @@ -420,7 +446,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c10 { + .c11 { width: auto; } } @@ -434,7 +460,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c11 { + .c12 { -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; @@ -446,57 +472,61 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` className="c0 c1 c2" >
- tick + tick + /> +
+ + Correct +
- - Correct - + Well done! +

-

- Well done! -

, .c0 { @@ -509,7 +539,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c10 { +.c11 { width: 100%; height: -webkit-fit-content; height: -moz-fit-content; @@ -517,14 +547,14 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c6 { +.c7 { position: relative; width: 100%; height: 100%; font-family: Lexend,sans-serif; } -.c5 { +.c6 { width: 1.5rem; height: 1.5rem; padding: 0rem; @@ -544,7 +574,18 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` gap: 1.5rem; } -.c11 { +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c12 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -562,7 +603,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` flex-grow: 1; } -.c4 { +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -570,7 +611,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` gap: 0.75rem; } -.c8 { +.c9 { color: #dd0035; font-family: Lexend,sans-serif; font-weight: 600; @@ -582,7 +623,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` letter-spacing: 0.0115rem; } -.c9 { +.c10 { margin-top: 0.75rem; font-family: Lexend,sans-serif; font-weight: 300; @@ -594,7 +635,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` letter-spacing: -0.005rem; } -.c7 { +.c8 { -webkit-filter: invert(98%) sepia(98%) saturate(0%) hue-rotate(328deg) brightness(102%) contrast(102%); filter: invert(98%) sepia(98%) saturate(0%) hue-rotate(328deg) brightness(102%) contrast(102%); object-fit: contain; @@ -605,7 +646,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c10 { + .c11 { width: auto; } } @@ -619,7 +660,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c11 { + .c12 { -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; @@ -631,57 +672,61 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` className="c0 c1 c2" >
- cross + cross + /> +
+ + Incorrect +
- - Incorrect - + Keep trying +

-

- Keep trying -

, .c0 { @@ -694,7 +739,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c10 { +.c11 { width: 100%; height: -webkit-fit-content; height: -moz-fit-content; @@ -702,7 +747,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c5 { +.c6 { width: 1.5rem; height: 1.5rem; padding: 0rem; @@ -711,7 +756,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c6 { +.c7 { position: relative; width: 100%; height: 100%; @@ -729,7 +774,18 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` gap: 1.5rem; } -.c11 { +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c12 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -747,7 +803,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` flex-grow: 1; } -.c4 { +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -755,7 +811,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` gap: 0.75rem; } -.c8 { +.c9 { color: #287c34; font-family: Lexend,sans-serif; font-weight: 600; @@ -767,7 +823,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` letter-spacing: 0.0115rem; } -.c9 { +.c10 { margin-top: 0.75rem; font-family: Lexend,sans-serif; font-weight: 700; @@ -779,7 +835,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` letter-spacing: -0.005rem; } -.c7 { +.c8 { -webkit-filter: invert(98%) sepia(98%) saturate(0%) hue-rotate(328deg) brightness(102%) contrast(102%); filter: invert(98%) sepia(98%) saturate(0%) hue-rotate(328deg) brightness(102%) contrast(102%); object-fit: contain; @@ -790,7 +846,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c10 { + .c11 { width: auto; } } @@ -804,7 +860,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c11 { + .c12 { -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; @@ -816,57 +872,61 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` className="c0 c1 c2" >
- tick + tick + /> +
+ + Correct +
- - Correct - + Nearly there +

-

- Nearly there -

, ] diff --git a/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.stories.tsx b/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.stories.tsx index 78d43d94..25d761df 100644 --- a/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.stories.tsx +++ b/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.stories.tsx @@ -60,3 +60,10 @@ export const AllFeedback: Story = { answerFeedback: "Good work!", }, }; + +export const WithNoAnswerFeedback: Story = { + args: { + feedback: "partially-correct", + answerFeedback: undefined, + }, +}; diff --git a/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.tsx b/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.tsx index c92a944d..eb3d322f 100644 --- a/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.tsx +++ b/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.tsx @@ -58,13 +58,15 @@ export const OakQuizFeedback = ({ {feedbackLabel}
- - {answerFeedback} - + {answerFeedback && ( + + {answerFeedback} + + )} ); }; From f282c4ed11b34aaa2260d87a79d9f2934cc63cfd Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Wed, 13 Mar 2024 09:44:43 +0000 Subject: [PATCH 14/15] fix(PUPIL-442): give the draggable feedback icon alt text --- .../molecules/OakDraggable/OakDraggable.tsx | 7 +- .../OakDraggableFeedback.test.tsx | 37 +++++ .../OakDraggableFeedback.tsx | 1 + .../OakDraggableFeedback.test.tsx.snap | 157 ++++++++++++++++++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 src/components/molecules/OakDraggableFeedback/OakDraggableFeedback.test.tsx create mode 100644 src/components/molecules/OakDraggableFeedback/__snapshots__/OakDraggableFeedback.test.tsx.snap diff --git a/src/components/molecules/OakDraggable/OakDraggable.tsx b/src/components/molecules/OakDraggable/OakDraggable.tsx index e70dc50a..3dfbf6a0 100644 --- a/src/components/molecules/OakDraggable/OakDraggable.tsx +++ b/src/components/molecules/OakDraggable/OakDraggable.tsx @@ -39,6 +39,10 @@ type OakDraggableProps = { * Icon color when not being dragged or hovered */ iconColor?: OakCombinedColorToken; + /** + * The alt text for the icon + */ + iconAlt?: string; /** * The background color of the draggable when not being dragged or hovered */ @@ -132,6 +136,7 @@ export const OakDraggable: FC< children, iconName = "move-arrows", iconColor = "icon-inverted", + iconAlt = "", color = "text-primary", background = "bg-primary", isDragging, @@ -162,7 +167,7 @@ export const OakDraggable: FC< iconName={iconName} $width="all-spacing-7" $height="all-spacing-7" - alt="" + alt={iconAlt} /> {children}
diff --git a/src/components/molecules/OakDraggableFeedback/OakDraggableFeedback.test.tsx b/src/components/molecules/OakDraggableFeedback/OakDraggableFeedback.test.tsx new file mode 100644 index 00000000..481ba5a7 --- /dev/null +++ b/src/components/molecules/OakDraggableFeedback/OakDraggableFeedback.test.tsx @@ -0,0 +1,37 @@ +import { create } from "react-test-renderer"; +import { ThemeProvider } from "styled-components"; +import React from "react"; +import "@testing-library/jest-dom"; + +import { OakDraggableFeedback } from "./OakDraggableFeedback"; + +import { oakDefaultTheme } from "@/styles"; +import renderWithTheme from "@/test-helpers/renderWithTheme"; + +describe(OakDraggableFeedback, () => { + it("matches snapshot", () => { + const tree = create( + + Elephant + , + ).toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it("applies an appropriate alt text for the icon", () => { + const { queryByAltText, rerender } = renderWithTheme( + Elephant, + ); + + expect(queryByAltText("correct")).toBeInTheDocument(); + + rerender( + + Elephant + , + ); + + expect(queryByAltText("incorrect")).toBeInTheDocument(); + }); +}); diff --git a/src/components/molecules/OakDraggableFeedback/OakDraggableFeedback.tsx b/src/components/molecules/OakDraggableFeedback/OakDraggableFeedback.tsx index a67c95cb..a680f05a 100644 --- a/src/components/molecules/OakDraggableFeedback/OakDraggableFeedback.tsx +++ b/src/components/molecules/OakDraggableFeedback/OakDraggableFeedback.tsx @@ -23,6 +23,7 @@ export const OakDraggableFeedback = ({ {...props} iconName={feedback === "correct" ? "tick" : "cross"} iconColor={feedback === "correct" ? "icon-success" : "icon-error"} + iconAlt={feedback === "correct" ? "correct" : "incorrect"} $ba="border-solid-xl" $borderColor={feedback === "correct" ? "border-success" : "border-error"} $background={feedback === "correct" ? "bg-correct" : "bg-incorrect"} diff --git a/src/components/molecules/OakDraggableFeedback/__snapshots__/OakDraggableFeedback.test.tsx.snap b/src/components/molecules/OakDraggableFeedback/__snapshots__/OakDraggableFeedback.test.tsx.snap new file mode 100644 index 00000000..22852818 --- /dev/null +++ b/src/components/molecules/OakDraggableFeedback/__snapshots__/OakDraggableFeedback.test.tsx.snap @@ -0,0 +1,157 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OakDraggableFeedback matches snapshot 1`] = ` +.c0 { + min-height: 3.5rem; + padding-top: 1.25rem; + padding-bottom: 1.25rem; + padding-left: 0.75rem; + padding-right: 1rem; + color: #222222; + background: #dff9de; + border: 0.25rem solid; + border-color: #287c34; + border-radius: 0.5rem; + font-family: Lexend,sans-serif; +} + +.c2 { + font-family: Lexend,sans-serif; +} + +.c5 { + position: relative; + width: 2rem; + height: 2rem; + font-family: Lexend,sans-serif; +} + +.c7 { + font-family: Lexend,sans-serif; + font-weight: 700; + font-size: 1.125rem; + line-height: 1.75rem; + -webkit-letter-spacing: -0.005rem; + -moz-letter-spacing: -0.005rem; + -ms-letter-spacing: -0.005rem; + letter-spacing: -0.005rem; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: 1rem; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c6 { + object-fit: contain; +} + +.c1 { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + outline: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c1:focus-visible:not([data-dragging="true"]):not([data-disabled="true"]) { + box-shadow: 0 0 0 0.125rem rgba(255,229,85,100%), 0 0 0 0.3rem rgba(87,87,87,100%); +} + +.c1[data-dragging="true"] { + cursor: move; + background-color: #bef2bd; + color: #222222; + outline: 0.25rem solid #222222; + outline-offset: -0.25rem; + box-shadow: 0.125rem 0.125rem 0 rgba(255,229,85,100%), 0.25rem 0.25rem 0 rgba(87,87,87,100%); + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c1[data-disabled="true"] { + cursor: default; + background-color: #f2f2f2; + color: #808080; +} + +.c1[data-readonly="true"] { + cursor: default; +} + +.c4 { + margin-block: -0.5rem; +} + +@media (hover:hover) { + .c1:hover:not([data-dragging="true"]):not([data-disabled="true"]):not( [data-readonly="true"] ) { + background-color: #dff9de; + color: #222222; + box-shadow: 0 0.5rem 0.5rem rgba(92,92,92,20%); + border-bottom: 0.25rem solid #222222; + padding-bottom: 1rem; + -webkit-text-decoration: underline; + text-decoration: underline; + } +} + +
+
+
+ correct +
+
+ Elephant +
+
+
+`; From 5d09e060f855ee4d2a9a7e2b58e38bce5a63621b Mon Sep 17 00:00:00 2001 From: Carl Whittaker Date: Wed, 13 Mar 2024 10:43:58 +0000 Subject: [PATCH 15/15] feat(PUPIL-442): give `OakQuizFeedback` an aria-live prop so the feedback will be read out by screen readers --- .../__snapshots__/OakLessonBottomNav.test.tsx.snap | 9 ++++++--- .../organisms/pupil/OakQuizFeedback/OakQuizFeedback.tsx | 3 ++- .../__snapshots__/OakQuizFeedback.test.tsx.snap | 9 ++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/organisms/pupil/OakLessonBottomNav/__snapshots__/OakLessonBottomNav.test.tsx.snap b/src/components/organisms/pupil/OakLessonBottomNav/__snapshots__/OakLessonBottomNav.test.tsx.snap index dc525a13..5770b0db 100644 --- a/src/components/organisms/pupil/OakLessonBottomNav/__snapshots__/OakLessonBottomNav.test.tsx.snap +++ b/src/components/organisms/pupil/OakLessonBottomNav/__snapshots__/OakLessonBottomNav.test.tsx.snap @@ -475,6 +475,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` className="c3 c4" >
tick
cross
tick +
tick
cross