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" /> + 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/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..3dfbf6a0 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,49 @@ type OakDraggableProps = { */ iconName?: IconName; /** - * Icon color + * Icon color when not being dragged or hovered */ - iconColor?: OakColorFilterToken; + iconColor?: OakCombinedColorToken; + /** + * The alt text for the icon + */ + iconAlt?: string; + /** + * 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 +90,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 +129,19 @@ export const OakDraggable: FC< ComponentPropsWithRef > = forwardRef< HTMLDivElement, - ComponentPropsWithoutRef + OakDraggableProps & ComponentPropsWithoutRef >( ( { children, iconName = "move-arrows", - iconColor = "icon-primary", + iconColor = "icon-inverted", + iconAlt = "", + color = "text-primary", + background = "bg-primary", isDragging, isDisabled, isReadOnly, - $borderColor = "transparent", ...props }, ref, @@ -111,23 +152,22 @@ 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} diff --git a/src/components/molecules/OakDraggable/__snapshots__/OakDraggable.test.tsx.snap b/src/components/molecules/OakDraggable/__snapshots__/OakDraggable.test.tsx.snap index 89547da3..30c96305 100644 --- a/src/components/molecules/OakDraggable/__snapshots__/OakDraggable.test.tsx.snap +++ b/src/components/molecules/OakDraggable/__snapshots__/OakDraggable.test.tsx.snap @@ -7,9 +7,8 @@ exports[`OakDraggable matches snapshot 1`] = ` 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; } @@ -64,6 +63,10 @@ exports[`OakDraggable matches snapshot 1`] = ` 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"]) { @@ -73,8 +76,12 @@ exports[`OakDraggable matches snapshot 1`] = ` .c1[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; } .c1[data-disabled="true"] { @@ -94,8 +101,12 @@ exports[`OakDraggable matches snapshot 1`] = ` @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; } } @@ -106,7 +117,7 @@ exports[`OakDraggable matches snapshot 1`] = ` className="c2 c3 c4" >
{ + 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 +
+
+
+`; diff --git a/src/components/molecules/OakDroppable/OakDroppable.stories.tsx b/src/components/molecules/OakDroppable/OakDroppable.stories.tsx index 09f1b24c..4b0aba67 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,16 @@ type Story = StoryObj; export const Default: Story = {}; -export const ADraggableHasEnteredTheDroppable: Story = { +export const Disabled: Story = { + args: { + isDisabled: true, + }, +}; + +/** + * A draggable has entered the droppable so it has entered an active state + */ +export const DraggingOver: Story = { args: { isOver: true, }, @@ -37,6 +47,48 @@ export const ADraggableHasEnteredTheDroppable: 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, }, }; +/** + * A draggable has entered the droppable so it has entered an active state + */ +export const DraggingOverWithSlotLabel: Story = { + args: { + canDrop: true, + isOver: true, + labelSlot: "never forgets", + }, +}; + +export const WithLongSlotLabel: Story = { + args: { + canDrop: true, + labelSlot: + "which animal never forgets and is the largest land animal on earth?", + }, +}; + +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 bddc5d4d..a513b62b 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,17 +16,28 @@ 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 + * + * 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")}; - } + &[data-disabled="true"] { + outline-color: ${parseColor("border-neutral")}; } `; @@ -37,28 +48,59 @@ 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, isDisabled, ...props }, ref) => { + const slotBackground = (() => { + if (isOver) { + return "bg-primary"; + } + if (!isDisabled) { + return "bg-neutral"; + } + + return "transparent"; + })(); + return ( - - - {children} - - + {children} + + {labelSlot && ( + + {labelSlot} + + )} + ); }); diff --git a/src/components/molecules/OakDroppable/__snapshots__/OakDroppable.test.tsx.snap b/src/components/molecules/OakDroppable/__snapshots__/OakDroppable.test.tsx.snap index 193a804c..7d002c30 100644 --- a/src/components/molecules/OakDroppable/__snapshots__/OakDroppable.test.tsx.snap +++ b/src/components/molecules/OakDroppable/__snapshots__/OakDroppable.test.tsx.snap @@ -4,37 +4,80 @@ exports[`OakDroppable matches snapshot 1`] = ` .c0 { padding: 1rem; background: #cee7e5; - border-radius: 0.375rem; + border-radius: 1rem; font-family: Lexend,sans-serif; } -.c1 { - width: 100%; +.c2 { min-height: 4rem; padding: 0.25rem; background: #f2f2f2; - border-radius: 0.375rem; + border-radius: 0.5rem; font-family: Lexend,sans-serif; } -.c2 { +.c5 { + width: 100%; + font-family: Lexend,sans-serif; +} + +.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-basis: 100%; + -ms-flex-preferred-size: 100%; + flex-basis: 100%; +} + +.c4 { outline: 0.188rem solid #222222; outline-style: dashed; } -@media (hover:hover) { - .c2:hover { - background-color: #ffffff; +.c4[data-disabled="true"] { + outline-color: #808080; +} + +@media (min-width:750px) { + .c1 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + } +} + +@media (min-width:1280px) { + .c1 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; } }
- Children +
+ Children +
`; 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..4407cc34 --- /dev/null +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/InternalDroppableHoldingPen.tsx @@ -0,0 +1,100 @@ +import React, { + ComponentPropsWithRef, + ComponentPropsWithoutRef, + FC, + forwardRef, + useLayoutEffect, + useState, +} from "react"; +import styled from "styled-components"; + +import { OakBox, OakFlex } from "@/components/atoms"; +import { parseColor } from "@/styles/helpers/parseColor"; + +const StyledOakBox = styled(OakBox)` + 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, 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 new file mode 100644 index 00000000..39c0bc65 --- /dev/null +++ b/src/components/organisms/pupil/InternalDroppableHoldingPen/__snapshots__/InternalDroppableHoldingPen.test.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InternalDroppableHoldingPen matches snapshot 1`] = ` +.c0 { + margin-bottom: 2rem; + border-radius: 1rem; + font-family: Lexend,sans-serif; +} + +.c2 { + min-height: 5rem; + padding: 0.75rem; + font-family: Lexend,sans-serif; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -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; +} + +.c1 { + background-color: #f2f2f2; + background-color: color-mix(in lch,#222222 5%,transparent); +} + +.c1[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"; 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,62 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` className="c0 c1 c2" >
- tick + + /> +
+ + Correct +
- - Correct - + Well done! +

-

- Well done! -

, .c0 { @@ -509,7 +540,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 +548,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 +575,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 +604,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` flex-grow: 1; } -.c4 { +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -570,7 +612,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` gap: 0.75rem; } -.c8 { +.c9 { color: #dd0035; font-family: Lexend,sans-serif; font-weight: 600; @@ -582,7 +624,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 +636,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 +647,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c10 { + .c11 { width: auto; } } @@ -619,7 +661,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 +673,62 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` className="c0 c1 c2" >
- cross + + /> +
+ + Incorrect +
- - Incorrect - + Keep trying +

-

- Keep trying -

, .c0 { @@ -694,7 +741,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 +749,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c5 { +.c6 { width: 1.5rem; height: 1.5rem; padding: 0rem; @@ -711,7 +758,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` font-family: Lexend,sans-serif; } -.c6 { +.c7 { position: relative; width: 100%; height: 100%; @@ -729,7 +776,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 +805,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` flex-grow: 1; } -.c4 { +.c5 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -755,7 +813,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` gap: 0.75rem; } -.c8 { +.c9 { color: #287c34; font-family: Lexend,sans-serif; font-weight: 600; @@ -767,7 +825,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 +837,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 +848,7 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` } @media (min-width:750px) { - .c10 { + .c11 { width: auto; } } @@ -804,7 +862,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 +874,62 @@ exports[`OakLessonBottomNav matches snapshot 1`] = ` className="c0 c1 c2" >
- 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..028f9cde 100644 --- a/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.tsx +++ b/src/components/organisms/pupil/OakQuizFeedback/OakQuizFeedback.tsx @@ -41,10 +41,11 @@ export const OakQuizFeedback = ({ } return ( - + - - {answerFeedback} - + {answerFeedback && ( + + {answerFeedback} + + )} ); }; diff --git a/src/components/organisms/pupil/OakQuizFeedback/__snapshots__/OakQuizFeedback.test.tsx.snap b/src/components/organisms/pupil/OakQuizFeedback/__snapshots__/OakQuizFeedback.test.tsx.snap index ce79635c..b4fa2a09 100644 --- a/src/components/organisms/pupil/OakQuizFeedback/__snapshots__/OakQuizFeedback.test.tsx.snap +++ b/src/components/organisms/pupil/OakQuizFeedback/__snapshots__/OakQuizFeedback.test.tsx.snap @@ -61,6 +61,7 @@ exports[`OakQuizFeedback matches snapshot 1`] = ` }
tick
cross
tick = { + 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..e114c54a --- /dev/null +++ b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.test.tsx @@ -0,0 +1,180 @@ +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"; + +// 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" }, + { 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..9460bc81 --- /dev/null +++ b/src/components/organisms/pupil/OakQuizMatch/OakQuizMatch.tsx @@ -0,0 +1,262 @@ +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 { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; + +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, { + coordinateGetter: sortableKeyboardCoordinates, + 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} is 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..150da921 --- /dev/null +++ b/src/components/organisms/pupil/OakQuizMatch/__snapshots__/OakQuizMatch.test.tsx.snap @@ -0,0 +1,743 @@ +// 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. +
+
, + .c6 { + font-family: Lexend,sans-serif; +} + +.c9 { + position: relative; + width: 2rem; + height: 2rem; + font-family: Lexend,sans-serif; +} + +.c0 { + margin-bottom: 2rem; + border-radius: 1rem; + font-family: Lexend,sans-serif; +} + +.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; + padding-left: 0.75rem; + padding-right: 1rem; + color: #ffffff; + background: #222222; + border-radius: 0.5rem; + font-family: Lexend,sans-serif; +} + +.c11 { + 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-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; +} + +.c7 { + 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; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c10 { + object-fit: contain; +} + +.c1 { + background-color: #f2f2f2; + background-color: color-mix(in lch,#222222 5%,transparent); +} + +.c1[data-over="true"] { + background-color: #ffffff; + background-color: color-mix( in lch, #ffffff 60%, transparent ); +} + +.c5 { + 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; +} + +.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%); +} + +.c5[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; +} + +.c5[data-disabled="true"] { + cursor: default; + background-color: #f2f2f2; + color: #808080; +} + +.c5[data-readonly="true"] { + cursor: default; +} + +.c8 { + margin-block: -0.5rem; +} + +@media (min-width:750px) { + +} + +@media (min-width:1280px) { + +} + +@media (hover:hover) { + .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%); + 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/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 91% rename from src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.test.tsx rename to src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx index dad700c1..b2fd4513 100644 --- a/src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.test.tsx +++ b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.test.tsx @@ -3,14 +3,17 @@ 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, -}); +window.matchMedia = + window.matchMedia ?? + jest.fn().mockReturnValue({ + matches: false, + }); describe(OakQuizOrder, () => { const initialItems = [ diff --git a/src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.tsx b/src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx similarity index 75% rename from src/components/organisms/pupil/OakQuizOrderQuestion/OakQuizOrder.tsx rename to src/components/organisms/pupil/OakQuizOrder/OakQuizOrder.tsx index 8b5e45f3..46c24bfa 100644 --- a/src/components/organisms/pupil/OakQuizOrderQuestion/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; @@ -62,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 ( - + { ); }; -/** - * 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 +93,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, )} - + ); @@ -200,23 +167,19 @@ function createAccouncements(items: OakQuizOrderItem[]): Announcements { items.findIndex((item) => 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, @@ -226,7 +189,6 @@ function createAccouncements(items: OakQuizOrderItem[]): Announcements { } }, onDragCancel({ active }) { - firstAnnouncement = true; return `Dragging was cancelled. Sortable item ${getItemLabel( active.id, )} was dropped.`; diff --git a/src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap b/src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap new file mode 100644 index 00000000..0a964ff8 --- /dev/null +++ b/src/components/organisms/pupil/OakQuizOrder/__snapshots__/OakQuizOrder.test.tsx.snap @@ -0,0 +1,619 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OakQuizOrder 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. +
+
, + .c0 { + font-family: Lexend,sans-serif; +} + +.c12 { + position: relative; + width: 2rem; + height: 2rem; + 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: #f2f2f2; + border-radius: 0.5rem; + font-family: Lexend,sans-serif; +} + +.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-radius: 0.5rem; + font-family: Lexend,sans-serif; +} + +.c14 { + 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-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%; +} + +.c10 { + 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; +} + +.c15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c13 { + object-fit: contain; +} + +.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; +} + +.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%); +} + +.c9[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; +} + +.c9[data-disabled="true"] { + cursor: default; + background-color: #f2f2f2; + color: #808080; +} + +.c9[data-readonly="true"] { + cursor: default; +} + +.c11 { + margin-block: -0.5rem; +} + +.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) { + .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; + } +} + +
+
+
+
+
+
+
+ +
+
+ Mouse +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ Hawk +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ Grasshopper +
+
+
+
+
+
+
, + @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/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/OakQuizOrderQuestion/__snapshots__/OakQuizOrder.test.tsx.snap b/src/components/organisms/pupil/OakQuizOrderQuestion/__snapshots__/OakQuizOrder.test.tsx.snap deleted file mode 100644 index f3b0bbf1..00000000 --- a/src/components/organisms/pupil/OakQuizOrderQuestion/__snapshots__/OakQuizOrder.test.tsx.snap +++ /dev/null @@ -1,545 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OakQuizOrder 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 (hover:hover) { - -} - -@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. -
-
, - .c0 { - font-family: Lexend,sans-serif; -} - -.c9 { - position: relative; - width: 2rem; - height: 2rem; - font-family: Lexend,sans-serif; -} - -.c2 { - padding: 1rem; - background: #cee7e5; - border-radius: 0.375rem; - font-family: Lexend,sans-serif; -} - -.c3 { - width: 100%; - min-height: 4rem; - padding: 0.25rem; - background: #f2f2f2; - border-radius: 0.375rem; - font-family: Lexend,sans-serif; -} - -.c5 { - min-height: 3.5rem; - padding-top: 1.25rem; - padding-bottom: 1.25rem; - padding-left: 0.75rem; - padding-right: 1rem; - background: #ffffff; - border-bottom: 0.25rem solid; - border-color: transparent; - border-radius: 0.5rem; - font-family: Lexend,sans-serif; -} - -.c11 { - 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-direction: column; - -ms-flex-direction: column; - flex-direction: column; - gap: 1rem; -} - -.c7 { - 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; -} - -.c12 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; -} - -.c10 { - object-fit: contain; -} - -.c6 { - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: grab; - outline: none; -} - -.c6: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"] { - cursor: move; - background-color: #bef2bd; - border: 0.25rem solid #222222; - box-shadow: 0.125rem 0.125rem 0 rgba(255,229,85,100%), 0.25rem 0.25rem 0 rgba(87,87,87,100%); -} - -.c6[data-disabled="true"] { - cursor: default; - background-color: #f2f2f2; - color: #808080; -} - -.c6[data-readonly="true"] { - cursor: default; -} - -.c8 { - margin-block: -0.5rem; -} - -.c4 { - 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 (hover:hover) { - .c4:hover { - background-color: #ffffff; - } -} - -
-
-
-
-
-
- -
-
- Mouse -
-
-
-
-
-
-
-
-
-
- -
-
- Hawk -
-
-
-
-
-
-
-
-
-
- -
-
- Grasshopper -
-
-
-
-
-
, - @media (hover:hover) { - -} - -@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 (hover:hover) { - -} - -@media (hover:hover) { - -} - -
, -] -`; diff --git a/src/components/organisms/pupil/index.ts b/src/components/organisms/pupil/index.ts index 1a315aeb..a81e4d3d 100644 --- a/src/components/organisms/pupil/index.ts +++ b/src/components/organisms/pupil/index.ts @@ -11,4 +11,5 @@ export * from "./OakHintButton"; export * from "./OakLessonNavItem"; export * from "./OakLessonReviewItem"; export * from "./OakLessonVideoTranscript"; -export * from "./OakQuizOrderQuestion"; +export * from "./OakQuizOrder"; +export * from "./OakQuizMatch"; 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. *