diff --git a/.cspell.json b/.cspell.json index e2f8ab0d6..0c958bbb4 100644 --- a/.cspell.json +++ b/.cspell.json @@ -64,6 +64,7 @@ "microbundle", "mixins", "multiselect", + "multiplechoice", "Myxococcus", "Patil", "popperjs", @@ -104,6 +105,11 @@ "wwnds", "wwnorton", "Yamanishi", - "zindex" + "zindex", + "Jayvon", + "Ibrahim", + "Huong", + "Urbach", + "Wiethe" ] } diff --git a/packages/core/src/_system.scss b/packages/core/src/_system.scss index 0e57f9763..cd1f849b8 100644 --- a/packages/core/src/_system.scss +++ b/packages/core/src/_system.scss @@ -35,6 +35,7 @@ @forward 'components/link' as link-*; @forward 'components/listbox' as listbox-*; @forward 'components/modal' as modal-*; +@forward 'components/multiple-choice' as multiple-choice-*; @forward 'components/popover' as popover-*; @forward 'components/popper' as popper-*; @forward 'components/progressbar' as progressbar-*; diff --git a/packages/core/src/components/field/index.scss b/packages/core/src/components/field/index.scss index 55d06eeff..c1b23bffe 100644 --- a/packages/core/src/components/field/index.scss +++ b/packages/core/src/components/field/index.scss @@ -24,7 +24,7 @@ @mixin info { @include type.ui-base; - :last-child { + & > *:last-child { margin-bottom: spacing.spacer('ui-inner'); } } @@ -87,7 +87,7 @@ display: flex; padding: var(--nds-field-padding-y) 0; - > * { + >* { margin-right: var(--nds-field-offset-x); } @@ -100,7 +100,7 @@ margin-right: unset; } - > .nds-field__input { + >.nds-field__input { // still position it in the center of the visual control so that form // validator tooltips will point at the right thing top: calc(var(--nds-font-size-base) * #{type.lh('ui')} / 2); diff --git a/packages/core/src/components/multiple-choice/index.scss b/packages/core/src/components/multiple-choice/index.scss new file mode 100644 index 000000000..97626d110 --- /dev/null +++ b/packages/core/src/components/multiple-choice/index.scss @@ -0,0 +1,48 @@ +@use '../../util'; + +@mixin style { + @include util.declare('multiple-choice') { + .nds-multiple-choice { + &__intro { + margin-bottom: 0.25rem; + } + + &__stem { + margin-bottom: 0.25rem; + font-weight: bold; + } + + &__instructions { + margin-bottom: 2rem; + font-size: var(--nds-font-size-xs); + } + + &__choice { + & > input, + & > label { + position: absolute; + + // FIXME: When adding a div as a children the hidden controls appear + // we need to fix this in the ChoiceField component + display: block; + width: 0; + height: 0; + visibility: hidden; + } + + & .nds-field__label { + display: flex; + gap: 1rem; + } + } + + &__feedback { + min-width: 5rem; + } + + &__choice-label { + font-weight: bold; + } + } + } +} diff --git a/packages/core/src/index.scss b/packages/core/src/index.scss index 4b17b7ff9..739d2fcfe 100644 --- a/packages/core/src/index.scss +++ b/packages/core/src/index.scss @@ -96,6 +96,7 @@ @include nds.link-style; @include nds.listbox-style; @include nds.modal-style; + @include nds.multiple-choice-style; @include nds.popover-style; @include nds.popper-style; @include nds.progressbar-style; diff --git a/packages/react/src/components/FeedbackModal/index.ts b/packages/react/src/components/FeedbackModal/index.ts index 39936bcc0..2bd7a2ac5 100644 --- a/packages/react/src/components/FeedbackModal/index.ts +++ b/packages/react/src/components/FeedbackModal/index.ts @@ -1 +1,2 @@ export { FeedbackModal } from './FeedbackModal'; +export type { FeedbackModalProps } from './types'; diff --git a/packages/react/src/components/ResponseIndicator/ResponseIndicator.tsx b/packages/react/src/components/ResponseIndicator/ResponseIndicator.tsx index 1ebf3b234..ab3a26729 100644 --- a/packages/react/src/components/ResponseIndicator/ResponseIndicator.tsx +++ b/packages/react/src/components/ResponseIndicator/ResponseIndicator.tsx @@ -18,13 +18,19 @@ export const ResponseIndicator: React.FC = ({ label, withIcon = true, placementIcon = 'left', + className, }) => { const uniqueId = useId(); const id = idProp || uniqueId; - const containerClassName = classNames(BASE_NAME, `${BASE_NAME}--${variant}`, { - [`${BASE_NAME}--${placementIcon}`]: withIcon, - }); + const containerClassName = classNames( + BASE_NAME, + `${BASE_NAME}--${variant}`, + { + [`${BASE_NAME}--${placementIcon}`]: withIcon, + }, + className, + ); return (
diff --git a/packages/react/src/components/ResponseIndicator/types.ts b/packages/react/src/components/ResponseIndicator/types.ts index 44bfd15b3..b318d3d3e 100644 --- a/packages/react/src/components/ResponseIndicator/types.ts +++ b/packages/react/src/components/ResponseIndicator/types.ts @@ -35,4 +35,6 @@ export interface ResponseIndicatorProps { * will override the default label */ label?: string; + + className?: string; } diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index c899334e7..d9eb9fed7 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -6,6 +6,7 @@ export * from './Checkbox'; export * from './ChoiceField'; export * from './Disclosure'; export * from './Dropdown'; +export * from './FeedbackModal'; export * from './Field'; export * from './Icon'; export * from './Link'; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index cbdf5e8c7..58c00f259 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -6,3 +6,5 @@ export * from './providers'; // Public utilities export * from './utilities'; + +export * from './patterns'; diff --git a/packages/react/src/patterns/MultipleChoice/AnswerChoice.tsx b/packages/react/src/patterns/MultipleChoice/AnswerChoice.tsx new file mode 100644 index 000000000..55b07768c --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/AnswerChoice.tsx @@ -0,0 +1,28 @@ +import React, { useCallback } from 'react'; +import { Radio } from '../../components/Radio'; +import { styles } from './styles'; +import { OnSelectInput } from './types'; + +interface AnswerChoiceProps { + label?: string; + checked?: boolean; + value?: number; + onSelect?: (input: OnSelectInput) => void; + children: React.ReactNode; +} + +export const AnswerChoice = ({ label, children, onSelect, value, checked }: AnswerChoiceProps) => { + const onChange = useCallback(() => { + if (value === undefined || !label || !onSelect) { + return; + } + onSelect({ index: value, label }); + }, [onSelect, value, label]); + + return ( + + {label}. + {children} + + ); +}; diff --git a/packages/react/src/patterns/MultipleChoice/Instructions.tsx b/packages/react/src/patterns/MultipleChoice/Instructions.tsx new file mode 100644 index 000000000..644160a34 --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/Instructions.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { styles } from './styles'; + +export const Instructions = ({ children }: React.PropsWithChildren) => ( +
{children}
+); diff --git a/packages/react/src/patterns/MultipleChoice/Intro.tsx b/packages/react/src/patterns/MultipleChoice/Intro.tsx new file mode 100644 index 000000000..10d3615a9 --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/Intro.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { styles } from './styles'; + +export const Intro = ({ children }: React.PropsWithChildren) => ( +
{children}
+); diff --git a/packages/react/src/patterns/MultipleChoice/MultipleChoice.tsx b/packages/react/src/patterns/MultipleChoice/MultipleChoice.tsx new file mode 100644 index 000000000..3d740e1b3 --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/MultipleChoice.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { RadioGroup } from '../../components/Radio'; +import { ResponseIndicator } from '../../components/ResponseIndicator'; +import { AnswerChoice } from './AnswerChoice'; +import { Instructions } from './Instructions'; +import { Intro } from './Intro'; +import { Stem } from './Stem'; +import { styles } from './styles'; +import { LabelType, OnSelectInput } from './types'; +import { resolveLabelType } from './utils'; + +export interface MultipleChoiceProps { + stem: string | React.ReactElement; + intro?: string | React.ReactElement; + instructions?: string | React.ReactElement; + choices: string[]; + /** + * @default 'lower-alpha' + */ + labelType?: LabelType; + status: 'correct' | 'incorrect' | 'unanswered'; + onSelect?: (input: OnSelectInput) => void; + selected?: number; + // TODO: support styling pieces +} + +export const MultipleChoice = ({ + stem, + intro, + instructions, + labelType = 'lower-alpha', + choices, + status, + onSelect, + selected, +}: MultipleChoiceProps) => { + const introElement = typeof intro === 'string' ? {intro} : intro; + const stemElement = typeof stem === 'string' ? {stem} : stem; + const instructionsElement = + typeof instructions === 'string' ? {instructions} : instructions; + + return ( +
+ {introElement} + {stemElement} + {instructionsElement} +
+ + {choices.map((choice, index) => { + const isCorrect = status === 'correct' && index === selected; + const isIncorrect = status === 'incorrect' && index === selected; + const label = resolveLabelType(labelType, index); + + let feedback: React.ReactNode = null; + if (isCorrect) { + feedback = ( + + ); + } else if (isIncorrect) { + feedback = ( + + ); + } else { + feedback =
; + } + + const checked = index === selected; + + // TODO: think a way to allow control of response indicator layout + // + // TODO: use grid to solve issues with incorrect response + // indicator + return ( +
+ {feedback} +
+ + {choice} + +
+
+ ); + })} + +
+
+ ); +}; diff --git a/packages/react/src/patterns/MultipleChoice/PatternExample.tsx b/packages/react/src/patterns/MultipleChoice/PatternExample.tsx new file mode 100644 index 000000000..95a494cb9 --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/PatternExample.tsx @@ -0,0 +1,43 @@ +import React, { useCallback } from 'react'; +import { Button } from '../../components/Button'; +import { FeedbackModal } from '../../components/FeedbackModal'; +import { MultipleChoice } from './MultipleChoice'; +import { useMultipleChoice } from './useMultipleChoice'; + +interface PatternExampleProps { + choices: string[]; +} + +export const PatternExample = ({ choices }: PatternExampleProps) => { + const { questionState, setStatus, modalState } = useMultipleChoice(choices); + + const handleSubmit = useCallback( + (event: React.FormEvent) => { + event.preventDefault(); + + if (questionState.selected === 1) { + setStatus('correct'); + } else { + setStatus('incorrect'); + } + }, + [setStatus, questionState.selected], + ); + + return ( +
+ + +
+ +
+ + ); +}; diff --git a/packages/react/src/patterns/MultipleChoice/Stem.tsx b/packages/react/src/patterns/MultipleChoice/Stem.tsx new file mode 100644 index 000000000..f4e5735f4 --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/Stem.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import { styles } from './styles'; + +export const Stem = ({ children }: React.PropsWithChildren) => ( +
{children}
+); diff --git a/packages/react/src/patterns/MultipleChoice/index.stories.tsx b/packages/react/src/patterns/MultipleChoice/index.stories.tsx new file mode 100644 index 000000000..337ddff1c --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/index.stories.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { MultipleChoice } from './MultipleChoice'; +import { PatternExample as PatternExampleComponent } from './PatternExample'; + +const meta: Meta = { + component: MultipleChoice, +}; + +export default meta; +type Story = StoryObj; + +const choices = [ + 'Jayvon, who had opened a checking account at the branch that same day', + 'Ibrahim, who is taking propranolol to control his blood pressure', + 'Huong, who was born with Urbach-Wiethe syndrome and lacks an amygdala', +]; + +export const Unanswered: Story = { + args: { + intro: + 'Three people are present when a pregnant person suddenly goes into labor and gives birth in a bank lobby.', + stem: 'Which of the people is likely to best remember the event afterward?', + instructions: 'Select one that applies. You have 2 attempts remaining.', + status: 'unanswered', + choices, + }, +}; + +export const Correct: Story = { + args: { + intro: + 'Three people are present when a pregnant person suddenly goes into labor and gives birth in a bank lobby.', + stem: 'Which of the people is likely to best remember the event afterward?', + instructions: 'Select one that applies. You have 2 attempts remaining.', + status: 'correct', + labelType: 'upper-roman', + selected: 2, + choices, + }, +}; + +export const Incorrect: Story = { + args: { + intro: + 'Three people are present when a pregnant person suddenly goes into labor and gives birth in a bank lobby.', + stem: 'Which of the people is likely to best remember the event afterward?', + instructions: 'Select one that applies. You have 2 attempts remaining.', + status: 'incorrect', + labelType: 'upper-alpha', + selected: 1, + choices, + }, +}; + +export const PatternExample: Story = { + render: () => , +}; diff --git a/packages/react/src/patterns/MultipleChoice/index.ts b/packages/react/src/patterns/MultipleChoice/index.ts new file mode 100644 index 000000000..b0d498d47 --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/index.ts @@ -0,0 +1,2 @@ +export * from './MultipleChoice'; +export * from './useMultipleChoice'; diff --git a/packages/react/src/patterns/MultipleChoice/styles.ts b/packages/react/src/patterns/MultipleChoice/styles.ts new file mode 100644 index 000000000..558e7671b --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/styles.ts @@ -0,0 +1,10 @@ +const prefix = 'nds-multiple-choice'; + +export const styles = { + stem: `${prefix}__stem`, + intro: `${prefix}__intro`, + instructions: `${prefix}__instructions`, + choiceLabel: `${prefix}__choice-label`, + feedback: `${prefix}__feedback`, + choice: `${prefix}__choice`, +}; diff --git a/packages/react/src/patterns/MultipleChoice/types.ts b/packages/react/src/patterns/MultipleChoice/types.ts new file mode 100644 index 000000000..7ef8254d0 --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/types.ts @@ -0,0 +1,8 @@ +export type LabelType = 'lower-alpha' | 'upper-alpha' | 'lower-roman' | 'upper-roman' | 'decimal'; + +export type OnSelectInput = { + label: string; + index: number; +}; + +export type MultipleChoiceStatus = 'unanswered' | 'correct' | 'incorrect'; diff --git a/packages/react/src/patterns/MultipleChoice/useMultipleChoice.tsx b/packages/react/src/patterns/MultipleChoice/useMultipleChoice.tsx new file mode 100644 index 000000000..372464c7e --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/useMultipleChoice.tsx @@ -0,0 +1,63 @@ +import { useState, useEffect, useMemo } from 'react'; +import { FeedbackModalProps } from '../../components/FeedbackModal/types'; +import { MultipleChoiceStatus, OnSelectInput } from './types'; + +interface MultipleChoiceState { + questionState: { + status: 'unanswered' | 'correct' | 'incorrect'; + onSelect?: (input: OnSelectInput) => void; + selected?: number; + choices: string[]; + }; + modalState: Pick< + FeedbackModalProps, + 'isOpen' | 'isCorrect' | 'choiceLabel' | 'choiceText' | 'onRequestClose' + >; + setStatus: (status: MultipleChoiceStatus) => void; +} + +export function useMultipleChoice(choices: string[]): MultipleChoiceState { + const [status, setStatus] = useState('unanswered'); + const [selected, setSelected] = useState(undefined); + const [modalOpen, setModalOpen] = useState(false); + + const questionState = useMemo(() => { + const onSelect = (input: OnSelectInput) => { + setSelected(input.index); + setStatus('unanswered'); + }; + + return { + status, + onSelect, + selected, + choices, + }; + }, [choices, selected, status]); + + const modalState = useMemo(() => { + return { + isOpen: modalOpen, + isCorrect: status === 'correct', + // TODO: pass label somehow + choiceLabel: 'a', + choiceText: selected !== undefined ? choices[selected] : '', + onRequestClose: () => { + setModalOpen(false); + }, + }; + }, [choices, modalOpen, selected, status]); + + useEffect(() => { + // TODO: we don't want to open the modal if the default state is not 'unanswered' + if (status !== 'unanswered') { + setModalOpen(true); + } + }, [status]); + + return { + questionState, + modalState, + setStatus, + }; +} diff --git a/packages/react/src/patterns/MultipleChoice/utils.ts b/packages/react/src/patterns/MultipleChoice/utils.ts new file mode 100644 index 000000000..f9d72db54 --- /dev/null +++ b/packages/react/src/patterns/MultipleChoice/utils.ts @@ -0,0 +1,20 @@ +import { LabelType } from './types'; + +const RomanLiterals = ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix', 'x']; + +export function resolveLabelType(labelType: LabelType, index: number) { + switch (labelType) { + case 'lower-alpha': + return String.fromCharCode(97 + index); + case 'upper-alpha': + return String.fromCharCode(65 + index); + case 'lower-roman': + return RomanLiterals[index]; + case 'upper-roman': + return RomanLiterals[index].toUpperCase(); + case 'decimal': + return String(index + 1); + default: + throw new Error(`Invalid label type: ${labelType}`); + } +} diff --git a/packages/react/src/patterns/index.ts b/packages/react/src/patterns/index.ts new file mode 100644 index 000000000..f217c827a --- /dev/null +++ b/packages/react/src/patterns/index.ts @@ -0,0 +1 @@ +export * from './MultipleChoice'; diff --git a/website/docs/patterns/index.mdx b/website/docs/patterns/index.mdx new file mode 100644 index 000000000..d2ec4f189 --- /dev/null +++ b/website/docs/patterns/index.mdx @@ -0,0 +1,16 @@ +--- +title: Patterns +slug: /patterns +--- + +import { Features, FeatureCard } from '@website/components'; + +> Components organized to offer an optimal UI solution based on best practices. + +Our patterns classify and document reusable solutions built to respond to common user scenarios. Following these guidelines allows us to create experiences that are consistent, accessible, and natural for users as they move between our digital products ensuring that our approach aligns with industry standards. + + + + A multiple choice question presents a question and set of predefined answer options, allowing for one answer choice submission. + + diff --git a/website/docs/patterns/multiple-choice-pattern.mdx b/website/docs/patterns/multiple-choice-pattern.mdx new file mode 100644 index 000000000..b93fd745c --- /dev/null +++ b/website/docs/patterns/multiple-choice-pattern.mdx @@ -0,0 +1,195 @@ +--- +title: Multiple Choice Pattern +description: A multiple choice question presents a question and set of predefined answer options, allowing for one answer choice submission. +--- + +import useBaseUrl from "@docusaurus/useBaseUrl"; +import { MultipleChoice, FeedbackModal } from "@wwnds/react"; +import { PropsTable } from "@website/components"; + +> A multiple choice question presents a question and set of predefined answer options, allowing for one answer choice submission. + +:::warning +This Multiple-Choice Pattern documentation has been released in Beta format. Questions or concerns? [Please let us know.](mailto:nds@wwnorton.com) +::: + +## Anatomy + +Anatomy of the Multiple Choice Pattern + +Anatomy of the Multiple Choice Pattern + +1. **Question Intro** (optional) - introductory content that helps frame the question. +1. **Question Stem** - the prompt the user responds to. +1. **Instructions** (optional) - text displaying how many attempts a user may submit an answer and instructions to how to complete a question\*, ideally located after the question stem. +1. **[Radio Button](../components/radio-group)** - an input that allows the user to select one answer and reflects the selection state. +1. **Answer Choice Identifier** - a letter or number that identifies each answer choice. +1. **Answer Choice** - a choice in response to the question stem that allows the user to select an answer. +1. **Response Indicator** (optional) - a label that signifies whether a selected response is correct or incorrect which appears next to the selected option after an answer has been submitted. +1. **Submit [Button](../components/button)** - a button that submits the chosen answer that can be located at the application level. +1. **Feedback [Modal](../components/modal)** - a modal dialog that provides feedback about the chosen answer after it is submitted. + - **a)** General Feedback - the chosen answer and whether it is correct or incorrect 1. Supplementary Feedback (optional) - additional answer-specific content + - **b)** Supplementary Feedback (optional) - additional answer-specific content + +## Usage + +- Multiple choice questions only have one correct answer, and use radio buttons (not buttons or dropdowns) to display answer choices. + + **Why:** The consistent use of radio buttons for answer choices, though beneficial for all users, has particular accessibility benefits: - Screen reader users know when they encounter radio buttons that they can only make one choice. - Since screen magnification users experience smaller portions of the screen at once,  they benefit from the visual consistency of always using radio buttons for answer choices. - For users who experience difficulty with attention and working memory, cognitive load is reduced when the same component is encountered repeatedly. See the [Accessibility Notes](#accessibility-notes) section for more information on how consistent navigation benefits users with disabilities. + +- Limit _answer choices_ to a maximum of 5. + + **Why:** Too much content can cause cognitive overload, loss of focus, and anxiety for students with disabilities, i.e. those with processing, attention, or working memory challenges. + +- Feedback should be delivered in a modal after the answer is submitted, as opposed to in-line.\* + + **Why:** In-line feedback presents challenges for many users: - Screen magnification users may miss that the screen content changed. - In-line feedback can make the screen visually overwhelming and increase cognitive load reducing the user's ability to gain the information intended by the in-line feedback. - Implementing in-line feedback presents challenges for screen reader users because you have to manage focus in ways that are unanticipated. See the [Accessibility Notes](#accessibility-notes) section for more information on the impact of in-line feedback on users with disabilities. + +- If more than one attempt is available, this should be clearly defined in the Instructions. The limit and the number of attempts they’ve already made should be indicated to the user. + + **Why:** Transparency and clear expectations are important to ensure all users can complete the task to the best of their ability. It would be frustrating for a user to find out there was a limit only after they reached it. + +- Provide clear instructions on how to complete a question. + + **Why:** Providing clear instructions to users on how to complete the question is key to ensure all users can complete the assigned task. + +- Multiple choice questions require a submit button either at the end of the answer choices or at the platform level. Selecting a response should never trigger a submit action. + + **Why:** Radio buttons are meant to make a selection only, not to submit an answer. For users with disabilities, the use of a submit button is anticipated and gives them control over when their context changes. + +:::danger Don't +Don’t use color alone for signifying correct and incorrect answers. INSTEAD include a visible “correct” or “incorrect” label for [response indicators](../components/response-indicator). + + **Why:** Users who can’t differentiate colors, such as colorblind users or those with visual impairments, won’t know which color is correct or incorrect. All users must have access to the same information. + - Text is the only way to guarantee that the meaning of the response indicator will be understood by all users. This supports low-vision users, English language learners, and those with cognitive disabilities. + +::: + +:::danger Don't +Don’t use dropdowns within multiple choice questions. INSTEAD use a [radio button](../components/radio-group). + + **Why:** Using a component like a dropdown doesn’t signal to the user that they can make only one choice and increases cognitive load by hiding answer choices. *Anticipated behavior by using the same design/consistency. + +::: + +## Content Guidelines + +- Question stems should be kept as brief as possible. + + **Why:** Clear and concise language ensures that all users understand what they are being asked to do. + +- Each answer option should be preceded by a corresponding letter or number, with a few exceptions (where the answer label could be confused with part of the answer choice). + + - The format of the answer label should be applied consistently throughout all multiple choice questions in an application. + + **Why:** This allows users to reference the option they’ve selected without reading the full answer text and allows users to easily differentiate between answer choices. For users who experience difficulty with attention and working memory, cognitive load is reduced when the same component is encountered repeatedly. + +- Answer-specific feedback should be concise and text-only is recommended. Refer to the [Accessibility Notes](#accessibility-notes) when including imagery or other media is necessary. + + **Why:** Users are less likely to consume the content if there’s too much of it, thus defeating the purpose of giving feedback. + +## Accessibility Notes + +This section expands on the design decisions and considerations made to ensure an inclusive and accessible experience for all users. + +### Inline Feedback + +Relying solely on in-line feedback can present challenges for users with disabilities for the following reasons: + +- Easily missed by users utilizing screen magnification: In-line feedback, particularly subtle visual cues or color changes, may become too small to notice when content is magnified. This can make it difficult for users relying on screen magnification to perceive and interpret the feedback, potentially leading to confusion or incomplete understanding. +- Limited perceptual cues in complex interfaces: In-line feedback, especially in interfaces with a lot of visual elements or complexity, may be too subtle for users with visual impairments or cognitive disabilities. These users may require more prominent or distinct cues to effectively grasp and comprehend the feedback provided. +- Potential Challenges in Screen Reader Navigation and Perception of In-Line Feedback: Screen reader users rely on auditory feedback to navigate interfaces. If they are not anticipating in-line feedback to appear, they may miss the alert or cue from the screen reader, resulting in a potential loss of understanding about the feedback's presence and location. + +### Alternative to Including Images or Other Media in Answer-Specific Feedback + +When possible, it is advisable to redirect students to relevant content in their eBook rather than embedding media or imagery within the feedback modal, as this approach enhances accessibility. However, if images or media are deemed absolutely essential, they must meet the following criteria: + +- **No Two-Way Scrolling:** In order to enhance accessibility, feedback should not require two-way scrolling, especially for screen magnification users. This restriction helps reduce cognitive load and completion time for all students. +- **No Color-Dependent Information:** It is strictly prohibited to use color alone in images or media to communicate information. This ensures accessibility for users with color blindness or other visual impairments. +- **Color Contrast Requirements:** Ensure that any images used are compliant with color contrast requirements, as they serve as informative elements rather than decorative features. +- **Alternative Text (Alt Text):** All images within feedback must have informative alt text to facilitate accessibility for users who rely on screen readers. +- **Embedded Media Caution:** It is generally discouraged to embed other media types in feedback. If required, follow these guidelines: + - **Labeling Controls:** Ensure all controls for embedded media are properly labeled. + - **WCAG 2.1 AA Compliance:** Embedded media (audio or video) should meet WCAG 2.1 AA requirements, including: + - Audio descriptions or descriptive transcripts for video content. + - Closed captions for audio or video content. + - Visible labels for any media player controls. + - Compliance with other relevant WCAG criteria. + +## React Implementation Example + +The following example demonstrates how to implement the Multiple Choice Pattern in a React application. + +A live demo can be found in the storybook for the Multiple Choice Pattern. + +```tsx +import React, { useCallback } from "react"; +import { + useMultipleChoice, + MultipleChoice, + FeedbackModal, + Button, +} from "@wwnds/react"; + +const choices = [ + "Jayvon, who had opened a checking account at the branch that same day", + "Ibrahim, who is taking propranolol to control his blood pressure", + "Huong, who was born with Urbach-Wiethe syndrome and lacks an amygdala", +]; + +export function PatternExample() { + const { questionState, setStatus, modalState } = useMultipleChoice(choices); + + const handleSubmit = useCallback( + (event: React.FormEvent) => { + event.preventDefault(); + + if (questionState.selected === 1) { + setStatus("correct"); + } else { + setStatus("incorrect"); + } + }, + [setStatus, questionState.selected] + ); + + return ( +
+ + +
+ +
+ + ); +} +``` + +### useMultipleChoice Hook + +The `useMultipleChoice` hook is used to manage the state of the multiple choice question. It accepts an array of answer choices and returns the state shared by the `FeedbackModal` and `MultipleChoice` components. + +### MultipleChoice Component + +The `MultipleChoice` component is used to render the multiple choice question. It accepts the following props: + + + +### FeedbackModal Component + +The `FeedbackModal` component is used to render the feedback modal. It accepts the following props: + + diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 5e9c2c9a4..cacd9ff46 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -74,6 +74,11 @@ const config = { label: "Components", position: "left", }, + { + to: "docs/patterns", + label: "Patterns", + position: "left", + }, { to: "docs/guides", label: "Guides", diff --git a/website/sidebars.js b/website/sidebars.js index 60e8038a1..29f43c575 100755 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -54,6 +54,7 @@ const sidebars = { "components/tooltip", "components/react-providers", ], + patterns: ["patterns/index", "patterns/multiple-choice-pattern"], }; module.exports = sidebars; diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 077b5ed0d..3e6e4ae2c 100755 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -31,6 +31,10 @@ const Home = (): JSX.Element => { The building blocks for composing applications and content with the Norton Design System. + + Components organized to offer an optimal UI solution based on best + practices. + Tutorials and examples to help designers, developers, and editors learn how get the most out of the Norton Design System. diff --git a/website/static/img/feedback-modal-anatomy.png b/website/static/img/feedback-modal-anatomy.png new file mode 100644 index 000000000..c1728ab1a Binary files /dev/null and b/website/static/img/feedback-modal-anatomy.png differ diff --git a/website/static/img/multiple-choice-anatomy.png b/website/static/img/multiple-choice-anatomy.png new file mode 100644 index 000000000..1cf762f3e Binary files /dev/null and b/website/static/img/multiple-choice-anatomy.png differ