arrow-left

All pages
gitbookPowered by GitBook
1 of 13

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

DNT-2657 Lists must be contained within semantically correct containers

src/components/MultiStepForm/components/ConfirmationStep/index.tsx

Description

Notes:

The information about call-time preference is visually displayed as a description list. In this case, it's a question and then an answer. This content is not marked up as a list.

When content is presented as a list, or a clearly related collection of items, marking it up using semantic list HTML elements helps assistive technology users to navigate and orient themselves within a page of content. Screen reader users can navigate to (or over) lists when they are correctly marked up. Also, when reading content in a list, assistive technology provides rich information about how long the list is and where the user is in the list. For example: "List, 9 items, Home, 1 of 9, About, 2 of 9".

This particular list is confusing because it is displayed even when there is no time indicated, like when a user chooses email as the preferred method of communication.

Recommendation

Remove the list entirely and write this information as a sentence. "We will reach out to you by phone during business hours," for example. If the user indicates that they prefer email, either write a new sentence indicating that preference, "One of our representatives will email you shortly," or remove the sentence altogether.

If you choose to keep the question and answer structure, use a description list (<dl>)for this data.

Code example (simplified)

Resources

chevron-rightDescription Listshashtag

Using description lists

Important Information about Techniques

See for important information about the usage of these informative techniques and how they relate to the normative WCAG 2.1 success criteria. The Applicability section explains the scope of the technique, and the presence of techniques for a specific technology does not imply that the technology can be used in all situations to create content that meets WCAG 2.1.

Applicability

HTML and XHTML

This technique relates to Success Criterion 3.1.3: Unusual Wordsarrow-up-right (Sufficient when used with G55: Linking to definitionsarrow-up-right).

Description

The objective of this technique is to provide the description of names or terms by presenting them in a description list. The list is marked up using the dl element. Within the list, each term is put in a separate dt element, and its description goes in the dd element directly following it. Multiple terms can be associated with a single description, as can a single term with multiple descriptions, provided that semantic sequence is maintained. The title attribute can be used to provide additional information about the description list. Usage of description lists ensures that terms and their descriptions are semantically related even as presentation format changes, as well as ensuring that these terms and descriptions are semantically grouped as a unit.

Description lists are easiest to use when the descriptions are ordered alphabetically. A common use for description lists is a glossary of terms.

NOTE

In HTML5 the name "Description list" was introduced. In previous versions these lists were referred to as "Definition lists".

Examples

Example 1

A list of descriptions of nautical terms used on a Website about sailing.

Resources

Resources are for information purposes only, no endorsement implied.

  • HTML5 Description listsarrow-up-right

  • HTML 4 Definition lists: the DL, DT, and DD elementsarrow-up-right

Related Techniques

  • G62: Providing a glossaryarrow-up-right

Tests

Procedure

For any set of terms and their associated descriptions:

  1. Check that the list is contained within a dl element.

  2. Check that each term in the list being described is contained within a dt element.

  3. Check that when there is more than one term that shares the same decription that the dt elements immediately follow each other.

  4. Check that the description for each term is contained in one or more dd elements.

  5. Check that the one or more dd elements immediately follow the one or more dt elements containing the term being described.

Expected Results

  • All checks above are true.

Using description listsarrow-up-right
Understanding Techniques for WCAG Success Criteriaarrow-up-right
<dl title="Nautical terms">
  <dt>Knot</dt>
  <dd>
    <p>A <em>knot</em> is a unit of speed equaling 1
      nautical mile per hour (1.15 miles per hour or 1.852
      kilometers per hour).</p>
  </dd>
  <dt>Port</dt>
  <dd>
    <p><em>Port</em> is the nautical term (used on
      boats and ships) that refers to the left side
      of a ship, as perceived by a person facing towards
      the bow (the front of the vessel).</p>
  </dd>
<dl>
    <dt>If contact by phone is your preference, what time of day is best?</dt>
    <dd>Daytime</dd>
</dl>
<dt>Starboard</dt>
<dd>
<p><em>Starboard</em> is the nautical term (used
on boats and ships) that refers to the right
side of a vessel, as perceived by a person
facing towards the bow (the front of the vessel).</p>
</dd>
</dl>

DNT-2860 Go Green Calculator

chevron-rightNew Featureshashtag

New Simple Calculator Input Warning And Block Explanation**

The need for this ticket derives from a fictional unit of energy duke created called a block which is "100 kWh of 'green' energy". Because a block requires whole-number outputs that necessitates a minimum input value; which arguably needs to be explained to the user, and if the user fails to heed the instructions... have the warning change color to red if they input a lower number.

Simple Calc Brancharrow-up-right

i.e. input modulo 100 should return a number not equal to input.

Additionally the calculator should include an explanation of the fictional unit being used in the output

i.e.

When you participate in the GoGreen Ohio program, you’re supporting the advancement of green power sources. Green power is electricity produced from natural resources – like the sun, wind, and water – that do not emit pollutants into the atmosphere.

As a GoGreen Ohio participant, you can match your home’s electricity use by purchasing blocks* of renewable energy certificates (RECs). These RECs certify the generation of green power on your behalf. Every block you purchase supports the advancement of environmentally friendly, renewable energy sources, thereby reducing dependence on fossil fuels to generate energy.

*One GoGreen Ohio program block represents 100 kWh of clean energy. A 10-block purchase (1,000 kWh) equals one REC (1MWh).

This will be a replacement of the existing calculators: Go Green: (Ohio jurisdiction, calc is under the Go Green Ohio video)

Renewable Advantage: (NC jurisdiction, look for "Calculate Your Estimated Cost"

    • Design questions contact: \

This will be a replacement of the existing calculators:

Go Green: (Ohio jurisdiction, calc is under the Go Green Ohio video)

Renewable Advantage: (NC jurisdiction, look for "Calculate Your Estimated Cost"

Design questions contact: Marcus Wilson

SimpleCalc-Min-Val
Go-Green(OH)
Renewable-Advantage(NK)

MatchElectricUsage

200kWh

250kWh

"The minimum kWh that can be entered is"

ChooseYourPayment

$2.00

$3.00

"The minimum amount that can be entered is $"

Abstractarrow-up-right
https://www.duke-energy.com/home/products/renewable-energy/gogreen-energyarrow-up-right
https://www.duke-energy.com/home/products/renewable-advantagearrow-up-right
marcus.wilson@duke-energy.comenvelope
https://app.abstract.com/projects/7d33aa49-f1f0-47eb-971d-893d6457bcbc/branches/4d491d6e-63f5-4bad-951e-0f327cfc047c/commits/afba0d4f418ba1a5f186815496c8331add993f74/files/56AED96A-C786-42FD-8B75-CFA17F1BE644/layers/BB077CE9-8557-4DDA-8C01-FC5575EE35A4?collectionId=123e3638-3117-4bbb-b7b0-8e43052397a8&collectionLayerId=6d234c60-58b2-4027-a631-e01d158248abarrow-up-right
https://www.duke-energy.com/home/products/renewable-energy/gogreen-energyarrow-up-right
https://www.duke-energy.com/home/products/renewable-advantagearrow-up-right

DNT-2785

/info/unindexed/dpa-conversion-business

The Image Slideshow component seems to have shrunk after JSS conversion. See it here: https://scprod-cms.duke-energy.com/info/unindexed/dpa-conversion-businessarrow-up-right, compared to pre-JSS: https://scdev28.duke-energy.com/info/unindexed/dpa-conversion-businessarrow-up-right. As you can see, it cuts off some of the image space and the description underneath is completely gone.

chevron-rightModal.tsxhashtag
import React, { useRef, PropsWithChildren } from 'react';
import { createPortal } from 'react-dom';
import { a11yAction } from 'src/lib/helpers';
import { Placeholder } from '@sitecore-jss/sitecore-jss-react';
import SvgLoader from 'src/components/SvgLoader';
import Transition from 'src/lib/Transition';
import { useBodyContext } from 'src/components/ContentWrapper/context';
import { useExperienceEditor } from 'src/lib/useExperienceEditor';
import useMediaQuery from 'src/lib/useMediaQuery';
import usePreventBodyScroll from 'src/lib/usePreventBodyScroll';
import { ModalComponentTypes, ModalTypes } from './types';
import { useFocusTrap } from 'src/lib/useFocusTrap';
const Portal = ({ children }: PropsWithChildren<{}>) => {
if (typeof window === 'undefined') return null;
// Create the root element that content is rendered into
// Used for "Portal" or other functionality that querySelectors for the '#root'
let portalRoot = document.getElementById('root');
if (!portalRoot) {
portalRoot = document.createElement('div');
portalRoot.setAttribute('id', 'root');
portalRoot = document.body.appendChild(portalRoot);
}
return createPortal(children, portalRoot);
};
const Modal = ({ id, rendering }: ModalTypes) => {
const { dispatch, state } = useBodyContext();
const { activeId, isOpen } = state.modal;
const { isEEActive } = useExperienceEditor();
const shouldDisplayModal = activeId === id && isOpen;
return isEEActive ? (
<div className="border">
The Modal Container
<Placeholder name="jss-public-modal-container" rendering={rendering} />
</div>
) : (
<ModalComponent
isActive={shouldDisplayModal}
onCloseHandler={() => dispatch({ type: 'modalClose' })}
{...{ id }}
>
<Placeholder name="jss-public-modal-container" rendering={rendering} />
</ModalComponent>
);
};
const ModalComponent = ({
children,
controls,
isActive,
onCloseHandler = () => {},
}: React.PropsWithChildren<ModalComponentTypes>) => {
const modalBackgroundRef = useRef<HTMLDivElement>(null);
const modalContainerRef = useRef<HTMLDivElement>(null);
const isMobile = !useMediaQuery('md');
// when the modal is active, do not allow the body to scroll
usePreventBodyScroll(isActive);
// classes for the modal 'background' div
const activeClass = isActive ? 'bg-black bg-opacity-80 pointer-events-auto z-modal' : '';
// classes for the modal 'container' div
const containerBackgroundClass = controls?.isTransparent ? '' : 'border border-gray bg-white';
const containerScrollClass = controls?.preventScroll ? '' : 'overflow-auto';
const containerClass = `${containerBackgroundClass} ${containerScrollClass}`;
// closes the modal when clicking outside of the modal
const handleClose = (event: React.MouseEvent<HTMLDivElement>) => {
if (
event &&
event.target &&
event.target instanceof Element &&
event.target.contains(modalBackgroundRef.current) &&
isActive
) {
onCloseHandler();
}
};
useFocusTrap({
shouldTrap: isActive,
container: modalBackgroundRef.current,
onExit: () => onCloseHandler(),
onEnter: ([first]) => first?.focus(),
});
return (
<Portal>
<div
className={`js-modal fixed h-screen w-full top-0 left-0 flex flex-col items-center p-12 pointer-events-none ${activeClass}`}
ref={modalBackgroundRef}
{...a11yAction(handleClose)}
role="dialog"
aria-hidden={!isActive}
tabIndex={-1}
>
<Transition.RevealDown
active={isActive}
className={`relative px-24 pt-32 pb-24 m-auto w-full max-w-3xl ${containerClass}`}
>
<div ref={modalContainerRef}>
<button
aria-label="close"
className="visible absolute right-0 top-0 mr-24 md:mt-16 mt-0 z-overlay"
onClick={onCloseHandler}
>
<SvgLoader
aria-hidden={true}
focusable={false}
color={controls?.isTransparent ? 'text-white' : 'text-teal-dark'}
name="X"
size={isMobile ? 20 : 24}
/>
</button>
{children}
</div>
</Transition.RevealDown>
</div>
</Portal>
);
};
export { Modal as default, ModalComponent };

DNT-2930 Hero Component

hashtag
Hero

Features a full-width image (light or dark) with superimposed text (light or dark contrasting). See a demo pagearrow-up-right with a site header.

hashtag
Usage Guidance

Do

  • Use on home pages and landing pages with multiple child pages.

  • When used on pages other than Home Pages or Section Landing Pages, the headline (H1) must be the page title.

  • Include a title/headline (required), a description (optional), and call-to-action link button(s) (optional).

Do Not

  • Use more than ONE Hero on a page.

Toggle Component Options

Theme Black to Gray-Darker Blue-Dark to Blue Blue to Teal-Darker Green-Dark to Green Blue-Dark to Green-Dark Green-Dark to Blue-Dark Gray-Light to white Gray-Light to Gray-Lighter Teal-Light to Teal-Lighter Image Business Solar Chef Family Landscape One Person Store Solar Solar Cell Two Person Store Content Content 1 Content 2 Content 3 Too Much Content Action No Actions Primary Action Primary and Secondary Action

hashtag
Say Yes to Renewables

Striving for sustainability is just smart business

hashtag
Accessibility

Labeling Expectations

  • Use a unique `id=""` on the title.

  • Use a unique `id=""` on the action link button(s).

  • Use `aria-labelledby=""` on the link buttons referencing the id of the link button(s) and the id of the title.

Keyboard Expectations

  • When you TAB into the `Hero` component, focus will go onto the link button(s).

Resources

2654-a11y-Audit-Form-Errors

Required and invalid form controls must convey/expose the fact that they are required and invalid

{% embed url="https: //scjsstest.duke-energy.com/home-services/gas-line-repair/enroll" %}

[![BSowa](https: //jiraprod.duke-energy.com/secure/useravatar?size=small&ownerId=bsowa&avatarId=12234)Sowa, Bill](https: //jiraprod.duke-energy.com/secure/ViewProfile.jspa?name=BSowa) created issue - Feb/10/22 8:20 AM[![BSowa](https: //jiraprod.duke-energy.com/secure/useravatar?size=xsmall&ownerId=bsowa&avatarId=12234)Sowa, Bill](https: //jiraprod.duke-energy.com/secure/ViewProfile.jspa?name=BSowa) made changes - Feb/10/22 8:33 AM

Initial Notes
Updated

Ensure that images clearly communicate the concept across all breakpoints and that critical information is not cropped out.

START NOWarrow-up-right
Web Accessibility: Linksarrow-up-right

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {{}} {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format </p>{code} {{}}

hashtag
[DXT Ninja Turtles](https:

//jiraprod.duke-energy.com/browse/DNT) / [DNT-2654 a11y audit - Form](https: //jiraprod.duke-energy.com/browse/DNT-2654) / [DNT-2655](https: //jiraprod.duke-energy.com/browse/DNT-2655)

Notes:

Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid.

Recommendation

Set aria-invalid="true" on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the aria-invalid attribute or set it to false.

Setting aria-invalid

<label for="address">Address</label> <input id="address" type="text" aria-invalid="true">

When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use aria-describedby to associate the error message with the form field. Once the field is valid, remove the error message.

Setting aria-describedby to point to an error message

Email <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format name@example.com

  • Options

6cbe92fc1812489eacd2322b8e5a2e2f

Notes: Invalid form controls are visually indicated as invalid, but this state is not communicated programmatically. Assistive technology users may have difficulty determining which controls are invalid. h5. Recommendation Set {{aria-invalid="true"}} on form fields that contain invalid data. This will ensure that the field is exposed to assistive technologies as an invalid field, and screen readers will announce this, for example, by announcing "invalid entry" as part of the field description. Once the field is valid, remove the {{aria-invalid}} attribute or set it to {{false}}. Setting aria-invalid {{<label for="address">Address</label>}} {{<input id="address" type="text" aria-invalid="true">}} When an inline error message communicates specific information (e.g., beyond the fact that the field was left blank), use {{aria-describedby}} to associate the error message with the form field. Once the field is valid, remove the error message. Setting {{aria-describedby}} to point to an error message {code:java} <label for="email">Email</label> <input id="email" type="email" aria-invalid="true" aria-describedby="email_error"> <p id="email_error">Please enter a valid email address in the format name@example.comenvelope</p>{code}

DNT-2724 Accordion Testing

chevron-right5 Things You Didn't Know About React Testing Libraryhashtag

Excerpt

I've just sold myself to the gods of click-baiting by making an "x things you didn't know about y" post. But hey, at least there no subtitle that says "number three will blow your mind!"


I've just sold myself to the gods of click-baiting by making an "x things you didn't know about y" post. But hey, at least there no subtitle that says "number three will blow your mind!"

Jokes aside the items in this list are concepts that I usually see beginners struggling with. At the same time, learning these concepts will vastyly improve your testing game. I know it did with mine.

1. Everything is a DOM node

This is usually the first misconception that beginners have when they start approaching Testing Library. It is especially true for those developers like me that came from .

Many think that getByText and other helper methods return some special wrapper around the React component. People even ask how to implement a getByReactComponent helper.

When you work with Testing Library you are dealing with . This is made clear by the first :

If it relates to rendering components, then it should deal with DOM nodes rather than component instances, and it should not encourage dealing with component instances.

If you want to check for yourself, it's as simple as this:

Once you realize that you're dealing with DOM nodes you can start taking advantage of all the DOM APIs like or 👍

2. debug's optional parameter

Since we now know that we're dealing with a DOM structure, it would be helpful to be able to "see" it. This is what is meant for:

ometimes thought debug's output can be very long and difficult to navigate. In those cases, you might want to isolate a subtree of your whole structure. You can do this easily by passing a node to debug:

3. Restrict your queries with within

Imagine you're testing a component that renders this structure:

You want to test that each ID gets its correct value. You can't use getByText('Apples') because there are two nodes with that value. Even if that wasn't the case you have no guarantee that the text is in the correct row.

What you want to do is to run getByText only inside the row you're considering at the moment. This is exactly what is for:

4. Queries accept functions too

You have probably seen an error like this one:

Usually, it happens because your HTML looks like this:

The solution is contained inside the error message: "[...] you can provide a function for your text matcher [...]".

What's that all about? It turns out matchers accept strings, regular expressions or functions.

The function gets called for each node you're rendering. It receives two arguments: the node's content and the node itself. All you have to do is to return true or false depending on if the node is the one you want.

An example will clarify it:

We're ignoring the content argument because in this case, it will either be "Hello", "world" or an empty string.

What we are checking instead is that the current node has the right . hasText is a little helper function to do that. I declared it to keep things clean.

That's not all though. Our div is not the only node with the text we're looking for. For example, body in this case has the same text. To avoid returning more nodes than needed we are making sure that none of the children has the same text as its parent. In this way we're making sure that the node we're returning is the smallest—in other words the one closes to the bottom of our DOM tree.

5. You can simulate browsers events with user-event

Ok, this one is a shameless plug since I'm the author of user-event. Still, people—myself included—find it useful. Maybe you will too.

All user-event tries to do is to simulate the events a real user would do while interacting with your application. What does it mean? Imagine you have an input field, and in your tests, you want to enter some text in it. You would probably do something like this:

It works but it doesn't simulate what happens in the browser. A real user would most likely move the mouse to select the input field and then start typing one character at the time. This, in turns, fires many events (blur, focus, mouseEnter, keyDown, keyUp...). user-event simulates all those events for you:

<!--
  Hero Section wrapper
  - Component below hero is required to use a white background color
-->

<section class="relative 2xl:px-24 2xl:pt-24">

  <!--
    Hero Container
  -->

  <div class="relative container-5xl flex flex-col justify-center aspect-16/12 sm:aspect-16/9 md:aspect-16/7 lg:aspect-16/6 xl:aspect-16/5 px-24 sm:px-32 md:px-48 py-64 md:py-48">

    <!--
      - Leave alt attribute empty
      - Use 'srcset' and 'sizes' attributes to optimize the hero image
    -->

    <img
      class="absolute top-0 left-0 object-cover object-right w-full h-full 2xl:rounded-lg bg-gray-light"
      src="image.jpg?w=800&as=1&bc=ffffff"
      srcset="image.jpg?w=800&as=1&bc=ffffff 800w,
              image.jpg?w=1600&as=1&bc=ffffff 1600w"
      sizes="(min-width: 768px) 1600px, 800px"
      alt=""
      width="1600"
      height="500"
      loading="lazy"
    >

    <!--
      Overlay
      - Change background gradient colors based on theme of hero
      - Default - "from-blue-dark to-blue sm:via-blue"
    -->

    <div
      class="absolute top-0 left-0 h-full w-full 2xl:rounded-l-lg md:w-3/4 bg-gradient-to-tr md:bg-gradient-to-r md:to-transparent opacity-80"
      aria-hidden="true"
    ></div>

    <!--
      Content Container
    -->

    <div class="relative w-full container-xs md:container-4xl">

      <!--
        If dark theme/gradient, text is white
          - Use "text-white"
        If light theme/gradient, text can be gray or blue
          - Use "text-gray-dark" -or- "text-blue"
        Default
          - "text-white"
      -->

      <div class="w-full md:w-1/2 text-center md:text-left">

        <!-- Hero Title
          - If this is the page title, you should use an <h1> element
          - Use an <h2> element if you already have a page title on the page
        -->
        <h2
          class="text-3xl md:text-2xl-fixed xl:text-3xl"
          id="hero-title"
        >
          Hero Title
        </h2>

        <!-- Hero Short Description-->
        <p class="text-lg xl:text-xl mt-12 lg:mt-16">
          Description goes here.
        </p>

        <!-- Hero Actions -->
        <div class="flex justify-center md:justify-start gap-16 lg:gap-24 mt-16 lg:mt-24">
          <!--
            If dark theme/gradient
              - Use "btn-primary-reversed"
            If light theme/gradient
              - Use "btn-primary"
            Default
              - "btn-primary-reversed"
          -->
          <a
            class="btn btn-primary-reversed btn-md"
            href="#"
            id="hero-primary-action"
            aria-labelledby="hero-primary-action hero-title"
          >
            Primary Action
          </a>
          <!--
            If dark theme/gradient
              - Use "btn-secondary-reversed"
            If light theme/gradient
              - Use "btn-secondary"
            Default
              - "btn-secondary-reversed"
          -->
          <a
            class="btn btn-secondary-reversed btn-sm lg:btn-md"
            href="#"
            id="hero-secondary-action"
            aria-labelledby="hero-secondary-action hero-title"
          >
            Secondary Action
          </a>
        </div>
      </div>

    </div>

  </div>

</section>
name@example.comenvelope
name@example.comenvelope
name@example.comenvelope
name@example.comenvelope
name@example.comenvelope
name@example.comenvelope
name@example.comenvelope
name@example.comenvelope
name@example.comenvelope
name@example.comenvelope

Jira Tickets

DNT-2658

Components must receive focus in an order that preserves meaning and operability

Unable to find an element with the text: Hello world.
This could be because the text is broken up by multiple elements.
In this case, you can provide a function for your text
matcher to make your matcher more flexible.
<div>Hello <span>world</span></div>
Enzymearrow-up-right
DOM nodesarrow-up-right
Guiding Principlearrow-up-right
querySelectorarrow-up-right
closestarrow-up-right
debugarrow-up-right
withinarrow-up-right
textContentarrow-up-right
import React from "react";
import { render, screen } from "@testing-library/react";

test("everything is a node", () => {
  const Foo = () => <div>Hello</div>;
  render(<Foo />);
  expect(screen.getByText("Hello")).toBeInstanceOf(Node);
});
import React from "react";
import { render, screen } from "@testing-library/react";

test("the button has type of reset", () => {
  const ResetButton = () => (
    <button type="reset">
      <div>Reset</div>
    </button>
  );
  render(<ResetButton />);
  const node = screen.getByText("Reset");

  // This won't work because `node` is the `<div>`
  // expect(node).toHaveProperty("type", "reset");

  expect(node.closest("button")).toHaveProperty("type", "reset"
});
const { debug } = render(<MyComponent />);
debug();
const { debug } = render(<MyComponent />);
const button = screen.getByText("Click me").closest();
debug(button);
<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>Fruit</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Apples</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Oranges</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Apples</td>
    </tr>
  </tbody>
</tabl
import React from "react";
import { render, screen, within } from "@testing-library/react"; // highlight-line
import "jest-dom/extend-expect";

test("the values are in the table", () => {
  const MyTable = ({ values }) => (
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Fruits</th>
        </tr>
      </thead>
      <tbody>
        {values.map(([id, fruit]) => (
          <tr key={id}>
            <td>{id}</td>
            <td>{fruit}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
  const values = [
    ["1", "Apples"],
    ["2", "Oranges"],
    ["3", "Apples"],
  ];
  render(<MyTable values={values} />);

  values.forEach(([id, fruit]) => {
    const row = screen.getByText(id).closest("tr");
    // highlight-start
    const utils = within(row);
    expect(utils.getByText(id)).toBeInTheDocument();
    expect(utils.getByText(fruit)).toBeInTheDocument();
    // highlight-end
  });
});
j;
import { render, screen, within } from "@testing-library/react";
import "jest-dom/extend-expect";

test("pass functions to matchers", () => {
  const Hello = () => (
    <div>
      Hello <span>world</span>
    </div>
  );
  render(<Hello />);

  // These won't match
  // getByText("Hello world");
  // getByText(/Hello world/);

  screen.getByText((content, node) => {
    const hasText = (node) => node.textContent === "Hello world";
    const nodeHasText = hasText(node);
    const childrenDontHaveText = Array.from(node.children).every(
      (child) => !hasText(child)
    );

    return nodeHasText && childrenDontHaveText;
  });
});
fireEvent.change(input, { target: { value: "Hello world" } });
import userEvent from "@testing-library/user-event";

userEvent.type(input, "Hello world");
DNT-2860 Go Green Calculator | DUKEbryan-guner.gitbook.iochevron-right

DNT-2843 EVCalculator

hashtag
Controls:

Name
Description
Default
Control

inputs*

DNT-2423 Throttle scroll event listeners

    // TODO Throttle event listeners
    window.addEventListener('scroll', handleScroll, { passive: true });

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);

  if (suppressNav || !hasSubPages) {
    return null;
  }

See: src/components/SecondaryNav/index.tsx - L:99

We probably don't need event fired for every single scroll.

chevron-rightSecondary Navhashtag
/* eslint-disable complexity */
/* eslint-disable max-lines */
import React

DNT-2659 A meaningful image must have a text alternative that serves the equivalent purpose

chevron-rightInfohashtag

Throtteling Event Listeners

chevron-rightThrottlehashtag

Using Throttling and Debouncing with React hooks - DEV Community

Excerpt

Throttling and debouncing techniques has been in use for past many years in javascript. In this post I'd like to share my knowledge on how we can use throttle and debounce functions with help of react hooks.

Consider below example with two routes

// # TextMatch type accenpts function: https://testing-library.com/docs/queries/about/#textmatch
import { renderWithCTX, screen, Matcher } from "src/lib/testWrappers";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import { Accordion, themeMap } from "./index";
import { compositionFunction } from "./composition";
import data from "./data";
import { stripHTMLTags } from "src/lib/helpers";
jest.mock("src/lib/useIntersection");
describe("Accordion", () => {
  const props = compositionFunction(data);
  it.skip("should render the correct items", () => {
    renderWithCTX(<Accordion {...props} />);
    for (let i = 0; i < props.items.length - 1; i++) {
      const accordionTitle = screen.getByText(
        props?.items[0]?.title?.value as string
      );
      expect(accordionTitle).toBeTruthy();
    }
  });
  it.skip("should render Accordion Component with image", () => {
    renderWithCTX(<Accordion {...props} />);
    const img = screen.getByRole("img", { name: /facebook/i });
    expect(img).toBeInTheDocument();
  });
  it("should reveal text when user clicks", async () => {
    renderWithCTX(<Accordion {...props} />);
    const accordionButton = screen.getByRole("button", {
      name: props.items[0].title?.value,
    });
    // # Full Manual Query:
    // (This is actually not good - getByText won't respect eg aria- attrs like getByRole so we aren't actually testing anything)
    // These are [not exposed to a11y tree](https://testing-library.com/docs/queries/about/#priority)
    // 👉
    // screen.getByText(
    //   /proin laoreet mauris vel urna tempor ultricies\. duis rhoncus lorem sed tellus egestas bibendum\. in aliquam mauris est, vel condimentum metus aliquet a\. nunc volutpat tincidunt nisl luctus pretium\. sagittis elit non, vestibulum metus\. mauris maximus vitae magna in mattis\. integer interdum maximus felis sed placerat\. nam lobortis tellus non felis fermentum, vitae venenatis ligula congue\. donec sit amet luctus odio\. magna commodo, sodales convallis ante scelerisque\. etiam ipsum lorem, rhoncus a sapien id, placerat consequat nunc\. curabitur in orci libero\. morbi tincidunt ante vel sem rutrum tempor\. cras ac purus quis urna maximus volutpat\./i
    // );
    // # We can still do a findByText,
    // but we need to check ourselves whether it is accessible:
    // 👉
    const hiddenText = await screen.findByText((content, element) => {
      const hasTextContent =
        element?.innerHTML === props?.items[0]?.text?.value;
      return hasTextContent;
    });
    expect(hiddenText).toBeInTheDocument();
    expect(hiddenText).toHaveAttribute("aria-hidden", "true");
    await userEvent.click(accordionButton);
    // # Compare against textContent stripHTMLTags:
    // (similar to https://polvara.me/posts/five-things-you-didnt-know-about-testing-library)
    // 👉
    // const noTag = stripHTMLTags(props?.items?.[0]?.text?.value as string);
    // const [revealedText] = await screen.findAllByText(
    //   // @ts-ignore
    //   // eslint-disable-next-line
    //   (content, element) => element?.textContent === noTag
    //   // ?  console.log('🔥', content) || true : false
    // );
    // expect(revealedText).toBeInTheDocument();
    // console.log('😳', noTag);
    // 😳 Proin laoreet mauris vel urna tempor ultricies. Duis rhoncus lorem sed tellus egestas bibendum. In aliquam mauris est, vel condimentum metus aliquet a. Nunc volutpat tincidunt nisl luctus pretium. Duis et ligula semper, sagittis elit non, vestibulum metus. Mauris maximus vitae magna in mattis. Integer interdum maximus felis sed placerat. Nam lobortis tellus non felis fermentum, vitae venenatis ligula congue. Donec sit amet luctus odio. Nam convallis justo vitae magna commodo, sodales convallis ante scelerisque. Etiam ipsum lorem, rhoncus a sapien id, placerat consequat nunc. Curabitur in orci libero. Morbi tincidunt ante vel sem rutrum tempor. Cras ac purus quis urna maximus volutpat.
    // # Find by innerHTML:
    // 👉
    const revealedText = await screen.findByText((content, element) => {
      const hasTextContent =
        element?.innerHTML === props?.items[0]?.text?.value;
      return hasTextContent;
    });
    expect(revealedText).toBeInTheDocument();
    expect(revealedText).toHaveAttribute("aria-hidden", "false");
  });
  // Overall, probably ultimately more durable / less error prone to use a testid and check aria- attrs
  it.skip("renders the correct default style", () => {
    const { rerender } = renderWithCTX(<Accordion {...props} />);
    const button = screen.getByRole("button", {
      name: /accordion trigger one/i,
    });
    expect(button).toHaveClass(themeMap.default.button);
    const altThemeProp = { ...props, theme: "footer" } as const;
    rerender(<Accordion {...altThemeProp} />);
    expect(button).toHaveClass(themeMap?.footer?.button);
  });
});

DNT-2658 Components must receive focus in an order that preserves meaning and operability

https://jiraprod.duke-energy.com/browse/DNT-2930jiraprod.duke-energy.comchevron-right
https://electron.duke-energy.com/components/hero/electron.duke-energy.comchevron-right
,
{
useState
,
useRef
,
useEffect
}
from
"
react
"
;
import { SecondaryNavType } from "./types";
import { NavItem } from "./components/NavItem";
import { DropDownMenu } from "./components/DropDownMenu";
import { useLocation } from "react-router-dom";
const SecondaryNav = ({ items, suppressNav }: SecondaryNavType) => {
const currentItems = items[0];
const [active, setActive] = useState(-1);
let { pathname } = useLocation();
pathname = pathname?.split(" ").join("-").toLowerCase();
const isItemActive = (index: number) => active === index;
// Need to check if the current page is the route of a top level page or any subpages or subpages' subpages so we can display the glorious teal bar
const selectedIndex: Array<number> = [];
let topLevelSelected: number;
if (currentItems) {
// check if the current page is one of the top level navItems in the blue bar
topLevelSelected = currentItems.subpages.findIndex(
(x) => x.route.toLowerCase() === pathname
);
// check if the current page is in any of the subpages
for (let i = 0; i < currentItems.subpages?.length; i++) {
const select = currentItems.subpages[i]?.subpages?.findIndex(
(x) => x.route.toLowerCase() === pathname
);
if (select !== -1) {
selectedIndex.push(i);
break;
} else {
for (
let ii = 0;
ii < currentItems.subpages[i]?.subpages[ii]?.subpages?.length;
ii++
) {
const select = currentItems.subpages[i]?.subpages[
ii
]?.subpages?.findIndex((x) => x.route.toLowerCase() === pathname);
if (select !== -1) {
selectedIndex.push(i);
break;
}
}
}
}
}
const isItemSelected = (index: number) => selectedIndex.indexOf(index) !== -1;
const hasSubPages = currentItems?.subpages.length > 0;
const getNumberOfCols = (index: number) => {
if (currentItems?.subpages[index].subpages[0]?.subpages?.length) {
return currentItems.subpages[index].subpages.length;
}
return 1;
};
const dropdownDirection = (index: number) => {
const navItemsLength = currentItems?.subpages.length;
const numberOfCols = getNumberOfCols(index);
const placeInNav = index + 1;
const displayToRight = navItemsLength - placeInNav >= numberOfCols;
const displayToLeft = placeInNav > numberOfCols;
const leftSide = placeInNav <= navItemsLength / 2;
if (displayToRight) {
return { side: "left-0", position: "relative", cols: numberOfCols };
} else if (displayToLeft) {
return { side: "right-0", position: "relative", cols: numberOfCols };
} else if (leftSide) {
return { side: "left-0", position: "", cols: numberOfCols };
} else {
return { side: "right-0", position: "", cols: numberOfCols };
}
};
const rootElement = useRef<HTMLDivElement | null>(null);
const activeButton = useRef<HTMLButtonElement>(null);
useEffect(() => {
const handleClick = (event: Event) => {
const cTarget = event.target as Element;
if (
(cTarget instanceof HTMLElement &&
!rootElement.current?.contains(cTarget)) ||
cTarget.tagName === "A" ||
cTarget.tagName === "SPAN"
) {
setActive(-1);
}
};
window.addEventListener("click", handleClick);
return () => {
window.removeEventListener("click", handleClick);
};
}, []);
useEffect(() => {
const handlePress = (event: KeyboardEvent) => {
const { key } = event;
if (key === "Escape") {
activeButton?.current?.focus();
setActive(-1);
}
};
window.addEventListener("keyup", handlePress);
return () => {
window.removeEventListener("keyup", handlePress);
};
}, []);
// Close menu when scrolled off screen
useEffect(() => {
const handleScroll = ({ currentTarget }: Event) => {
if (!rootElement.current) {
return;
}
const headerDimensions = rootElement.current.getBoundingClientRect();
const headerDistanceFromTop = headerDimensions.top;
const headerHeight = headerDimensions.height;
const scrollingDown = (currentTarget as Window).scrollY > 0;
const headerOutOfView = headerDistanceFromTop + headerHeight < 0;
// offScreen
const offScreen = headerOutOfView && scrollingDown;
if (offScreen) {
// Close menu
setActive(-1);
}
};
// TODO Throttle event listeners
window.addEventListener("scroll", handleScroll, { passive: true });
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
if (suppressNav || !hasSubPages) {
return null;
}
return (
<div
className="hidden xl:block xl:-mt-px px-16 md:px-24 bg-blue text-white"
ref={rootElement}
>
<nav className="relative container-4xl h-full" aria-label="Secondary">
<ul className="flex justify-between -mx-16 h-full">
{currentItems.subpages.map(({ subpages, ...rest }, index) => {
const isActive = isItemActive(index);
const isSelected =
isItemSelected(index) || topLevelSelected === index;
return (
<li className={dropdownDirection(index).position} key={index}>
<NavItem
{...rest}
currentRef={isActive ? activeButton : null}
isActive={isActive}
isSelected={isSelected}
onClick={() => setActive(isActive ? -1 : index)}
type={subpages.length ? "button" : "link"}
/>
<DropDownMenu
{...{
subpages,
...{
...rest,
isActive,
dropdownDirection: dropdownDirection(index),
},
}}
/>
</li>
);
})}
</ul>
</nav>
</div>
);
};
export default SecondaryNav;
https://jiraprod.duke-energy.com/browse/DNT-2423jiraprod.duke-energy.comchevron-right

InputsType

-

-

output*

YearlyOutputProps

-

-

analytics

ComponentEvent

-

-

backgroundColor

string

-

-

bgColorClass

"""bg-gray-lighter"

-

-

ctaText

any

-

-

fullWidth

boolean

-

-

image

any

-

-

isCtaAButton

boolean

-

-

link

any

-

-

mobileLink

any

-

-

modal

{ id: string; url?: string; } | null

-

-

subtitle

any

-

-

title

any

-

-

file-archive
4KB
EVCalculator.zip
archive
arrow-up-right-from-squareOpen
file-archive
8KB
Calculator.zip
archive
arrow-up-right-from-squareOpen
//Calculator/index.tsx

import React from "react";
import Input from "./inputComponents/Input";
import Stepper from "./inputComponents/Stepper";
import Slider from "./inputComponents/Slider";

const Calculator = () => (
  <section className="px-16 md:px-24 py-32 md:py-48">
    <div className="mb-48 items-center flex">
      <Slider
        prompt="Prompt goes here"
        defaultVal="88888"
        min="0"
        max="100000"
        label="Number Slider Title"
      />
    </div>
    <div className="flex flex-1">
      <Stepper prompt="Prompt goes here" defaultVal="1" />
      <Input
        prompt="Prompt goes here"
        defaultVal="500"
        label="per month"
        isInDollars={true}
      />
    </div>
  </section>
);

export default Calculator;
//src/components/Calculator/outputComponents/YearlySavings.tsx

import { YearlyOutputProps } from "../types";

const avgDaysInMonth = 30.437;
const avgDaysInYear = 365.25;

const formatMoney = (money: number) => {
  const roundedNum = Math.round(money * 100) / 100;
  const arr = roundedNum.toLocaleString().split(".");
  const cents = `${arr[1] || ""}00`.slice(0, 2);
  const dollars = arr[0];
  return {
    dollars,
    cents,
    full: `$${dollars}.${cents}`,
  };
};

const YearlySavings = ({
  dailySavings = 0,
  headline = "Total Yearly Savings",
  monthlyText = "Monthly Savings",
  dailyText = "Daily Savings",
  description,
}: YearlyOutputProps) => {
  const annual = formatMoney(dailySavings * avgDaysInYear);
  const monthly = formatMoney(dailySavings * avgDaysInMonth);
  const daily = formatMoney(dailySavings);

  return (
    <div>
      <h4 className="text-blue text-xl-fixed">{headline}</h4>
      {/* need to adjust line height to vertically top align */}
      <p
        className="text-blue my-12 text-2xl-fixed align-top"
        style={{ lineHeight: "55px" }}
      >
        $
        <span className="text-3xl-fixed align-top leading-none">
          {annual.dollars}
        </span>
        {`.${annual.cents}`}
      </p>
      <div className="flex flex-row justify-center">
        <div className="border-r px-32 border-gray">
          <p
            className="text-blue align-top text-lg-fixed"
            style={{ lineHeight: "25px" }}
          >
            $<span className="text-xl-fixed align-top">{daily.dollars}</span>
            {`.${daily.cents}`}
          </p>
          <p className="text-gray-dark text-xs">{dailyText}</p>
        </div>
        <div className="px-32">
          <p
            className="text-blue align-top text-lg-fixed"
            style={{ lineHeight: "25px" }}
          >
            $<span className="text-xl-fixed align-top">{monthly.dollars}</span>
            {`.${monthly.cents}`}
          </p>
          <p className="text-gray-dark text-xs">{monthlyText}</p>
        </div>
      </div>
      {description && (
        <p className="mt-32 text-gray-dark text-md">{description}</p>
      )}
    </div>
  );
};

export default YearlySavings;
//src/components/Calculator/test.tsx

import "@testing-library/jest-dom";
import Input from "src/components/Calculator/inputComponents/Input";
import Slider from "src/components/Calculator/inputComponents/Slider";
import YearlySavings from "./outputComponents/YearlySavings";
import { renderWithCTX, screen, fireEvent } from "src/lib/testWrappers";

const props: Parameters<typeof Input>[0] = {
  prompt: "Current Gas Price",
  defaultVal: "3.12",
  label: "per gallon",
  isInDollars: true,
};

describe("calculator input", () => {
  it("should render", () => {
    renderWithCTX(<Input {...props} />);
    const input = screen.getByRole("textbox", {
      name: `${props?.prompt} ${props?.label}`,
    });
    expect(input).toBeInTheDocument();
    expect(input).toHaveAttribute("value", props.defaultVal);
  });
  it("should prefix with $ if applicable", () => {
    const { rerender } = renderWithCTX(<Input {...props} />);
    let currency: HTMLElement | null = screen.getByText(/\$/i);
    expect(currency).toBeInTheDocument();
    rerender(<Input {...props} isInDollars={false} />);
    currency = screen.queryByText(/\$/i);
    expect(currency).not.toBeInTheDocument();
  });
});

describe("calculator slider input", () => {
  const props = {
    prompt: "How many miles do you drive daily?",
    defaultVal: "160",
    label: "Daily Miles",
    min: "0",
    max: "320",
    step: "1",
  };
  it("should render", () => {
    renderWithCTX(<Slider {...props} />);
    const slider = screen.getByRole("slider", {
      name: `${props.prompt} ${props.label}`,
    });
    const input = screen.getByRole("textbox", {
      name: `${props.prompt} ${props.label}`,
    });
    const label = screen.getByText(/daily miles/i);
    const heading = screen.getByRole("heading", {
      name: /how many miles do you drive daily\?/i,
    });
    expect(slider).toBeInTheDocument();
    expect(input).toBeInTheDocument();
    expect(label).toBeInTheDocument();
    expect(heading).toBeInTheDocument();
  });
  it("should default to midpoint between min and max when defaultVal is missing", () => {
    renderWithCTX(<Slider {...props} defaultVal="" min="100" max="300" />);
    const slider = screen.getByRole("slider", {
      name: `${props.prompt} ${props.label}`,
    });
    expect(slider).toHaveAttribute("value", "200");
  });
  it("should keep the same slider and text input values", () => {
    renderWithCTX(<Slider {...props} />);
    const slider = screen.getByRole("slider", {
      name: `${props.prompt} ${props.label}`,
    });
    const input = screen.getByRole("textbox", {
      name: `${props.prompt} ${props.label}`,
    });
    // slider changes input
    fireEvent.change(slider, { target: { value: 28 } });
    expect(input).toHaveAttribute("value", "28");
    // input changes slider
    fireEvent.change(input, { target: { value: 140 } });
    expect(slider).toHaveAttribute("value", "140");
  });
});

describe("yearly savings output", () => {
  const props = {
    headline: "Yearly Savings",
    monthlyText: "Monthly Savings",
    dailyText: "Daily Savings",
    dailySavings: 2.22,
  };
  it("should render", () => {
    renderWithCTX(<YearlySavings {...props} />);
    const heading = screen.getByRole("heading", { name: props.headline });
    const daily = screen.getByText(props.dailyText);
    const monthly = screen.getByText(props.monthlyText);
    expect(heading).toBeInTheDocument();
    expect(daily).toBeInTheDocument();
    expect(monthly).toBeInTheDocument();
  });
  it("should properly display rounded monetary amounts", () => {
    renderWithCTX(<YearlySavings dailySavings={7.998} />);
    const dollars = screen.getByText("8");
    const cents = screen.getByText(/\$\.00/i);
    expect(dollars).toBeInTheDocument();
    expect(cents).toBeInTheDocument();
  });
});
// Stepper Test.tsx



import '@testing-library/jest-dom';
import Input from 'src/components/Calculator/inputComponents/Input';
import Slider from 'src/components/Calculator/inputComponents/Slider';
import YearlySavings from './outputComponents/YearlySavings';
import { renderWithCTX, screen, fireEvent } from 'src/lib/testWrappers';

const props: Parameters<typeof Input>[0] = {
  prompt: 'Current Gas Price',
  defaultVal: '3.12',
  label: 'per gallon',
  isInDollars: true,
};

describe('calculator input', () => {
  it('should render', () => {
    renderWithCTX(<Input {...props} />);
    const input = screen.getByRole('textbox', {
      name: `${props?.prompt} ${props?.label}`,
    });
    expect(input).toBeInTheDocument();
    expect(input).toHaveAttribute('value', props.defaultVal);
  });
  it('should prefix with $ if applicable', () => {
    const { rerender } = renderWithCTX(<Input {...props} />);
    let currency: HTMLElement | null = screen.getByText(/\$/i);
    expect(currency).toBeInTheDocument();
    rerender(<Input {...props} isInDollars={false} />);
    currency = screen.queryByText(/\$/i);
    expect(currency).not.toBeInTheDocument();
  });
});

describe('calculator slider input', () => {
  const props = {
    prompt: 'How many miles do you drive daily?',
    defaultVal: '160',
    label: 'Daily Miles',
    min: '0',
    max: '320',
    step: '1',
  };
  it('should render', () => {
    renderWithCTX(<Slider {...props} />);
    const slider = screen.getByRole('slider', { name: `${props.prompt} ${props.label}` });
    const input = screen.getByRole('textbox', { name: `${props.prompt} ${props.label}` });
    const label = screen.getByText(/daily miles/i);
    const heading = screen.getByRole('heading', { name: /how many miles do you drive daily\?/i });
    expect(slider).toBeInTheDocument();
    expect(input).toBeInTheDocument();
    expect(label).toBeInTheDocument();
    expect(heading).toBeInTheDocument();
  });
  it('should default to midpoint between min and max when defaultVal is missing', () => {
    renderWithCTX(<Slider {...props} defaultVal="" min="100" max="300" />);
    const slider = screen.getByRole('slider', { name: `${props.prompt} ${props.label}` });
    expect(slider).toHaveAttribute('value', '200');
  });
  it('should keep the same slider and text input values', () => {
    renderWithCTX(<Slider {...props} />);
    const slider = screen.getByRole('slider', { name: `${props.prompt} ${props.label}` });
    const input = screen.getByRole('textbox', { name: `${props.prompt} ${props.label}` });
    // slider changes input
    fireEvent.change(slider, { target: { value: 28 } });
    expect(input).toHaveAttribute('value', '28');
    // input changes slider
    fireEvent.change(input, { target: { value: 140 } });
    expect(slider).toHaveAttribute('value', '140');
  });
});

describe('yearly savings output', () => {
  const props = {
    headline: 'Yearly Savings',
    monthlyText: 'Monthly Savings',
    dailyText: 'Daily Savings',
    dailySavings: 2.22,
  };
  it('should render', () => {
    renderWithCTX(<YearlySavings {...props} />);
    const heading = screen.getByRole('heading', { name: props.headline });
    const daily = screen.getByText(props.dailyText);
    const monthly = screen.getByText(props.monthlyText);
    expect(heading).toBeInTheDocument();
    expect(daily).toBeInTheDocument();
    expect(monthly).toBeInTheDocument();
  });
  it('should properly display rounded monetary amounts', () => {
    renderWithCTX(<YearlySavings dailySavings={7.998} />);
    const dollars = screen.getByText('8');
    const cents = screen.getByText(/\$\.00/i);
    expect(dollars).toBeInTheDocument();
    expect(cents).toBeInTheDocument();
  });
});



describe('calculator slider input', () => {
  const props = {
    prompt: 'How many miles do you drive daily?',
    defaultVal: '160',
    label: 'Daily Miles',
    min: '0',
    max: '320',
    step: '1',
  };
  it('should render', () => {
    renderWithCTX(<Slider {...props} />);
    const slider = screen.getByRole('slider', { name: `${props.prompt} ${props.label}` });
    const input = screen.getByRole('textbox', { name: `${props.prompt} ${props.label}` });
    const label = screen.getByText(/daily miles/i);
    const heading = screen.getByRole('heading', { name: /how many miles do you drive daily\?/i });
    expect(slider).toBeInTheDocument();
    expect(input).toBeInTheDocument();
    expect(label).toBeInTheDocument();
    expect(heading).toBeInTheDocument();
  });
  it('should default to midpoint between min and max when defaultVal is missing', () => {
    renderWithCTX(<Slider {...props} defaultVal="" min="100" max="300" />);
    const slider = screen.getByRole('slider', { name: `${props.prompt} ${props.label}` });
    expect(slider).toHaveAttribute('value', '200');
  });
  it('should keep the same slider and text input values', () => {
    renderWithCTX(<Slider {...props} />);
    const slider = screen.getByRole('slider', { name: `${props.prompt} ${props.label}` });
    const input = screen.getByRole('textbox', { name: `${props.prompt} ${props.label}` });
    // slider changes input
    fireEvent.change(slider, { target: { value: 28 } });
    expect(input).toHaveAttribute('value', '28');
    // input changes slider
    fireEvent.change(input, { target: { value: 140 } });
    expect(slider).toHaveAttribute('value', '140');
  });
});

describe('yearly savings output', () => {
  const props = {
    headline: 'Yearly Savings',
    monthlyText: 'Monthly Savings',
    dailyText: 'Daily Savings',
    dailySavings: 2.22,
  };
  it('should render', () => {
    renderWithCTX(<YearlySavings {...props} />);
    const heading = screen.getByRole('heading', { name: props.headline });
    const daily = screen.getByText(props.dailyText);
    const monthly = screen.getByText(props.monthlyText);
    expect(heading).toBeInTheDocument();
    expect(daily).toBeInTheDocument();
    expect(monthly).toBeInTheDocument();
  });
  it('should properly display rounded monetary amounts', () => {
    renderWithCTX(<YearlySavings dailySavings={7.998} />);
    const dollars = screen.getByText('8');
    const cents = screen.getByText(/\$\.00/i);
    expect(dollars).toBeInTheDocument();
    expect(cents).toBeInTheDocument();
  });
});
)
;
Five Things You (Probably) Didn't Know About Testing Librarypolvara.mechevron-right
  • Projectsarrow-up-right

  • Issuesarrow-up-right

  • Boardsarrow-up-right

  • Time in Statusarrow-up-right

  • Createarrow-up-right

  • Give feedback to Atlassianarrow-up-right

  • Helparrow-up-right

  • Administrationarrow-up-right

  • arrow-up-right

  • Welcome to Duke Energy Enterprise JIRA ~ Please enter a ticket through MyIT if you have any significant problems with the tool. Thank you! Have a great day!

    arrow-up-right

    DXT Ninja Turtlesarrow-up-right

    ======================================================================================================

    DNT boardarrow-up-right

    • User story maparrow-up-right

    • Kanban boardarrow-up-right

    • Releasesarrow-up-right

    1. DXT Ninja Turtlesarrow-up-right

    2. DNT-2654 a11y audit - Formarrow-up-right

    3. DNT-2659arrow-up-right

    Details

    • Type: Sub-task

    • Status:REVIEW (View Workflowarrow-up-right)

    • Priority: Medium

    • Resolution:Unresolved

    • Labels:

      None

    Description

    (MultiStepForm issue) (Stepper component)

    Notes:

    The check mark that indicates a completed step in the form is an <svg> image without a text alternative. Blind and low vision users will not know that this is a completed step.

    Recommendation

    Use the SVG <title> element to provide a text alternative that's appropriate for the context. To ensure support in assistive technologies, provide role="img" and aria-labelledby attributes in the <svg> element.

    SVG with <title> element, role="img", and the aria-labelledby attribute (Simplified code)

    Complete ...

    Contextually Marking up accessible images and SVGsarrow-up-right

    • Options

    Attachments

    Drop files to attach, or browse.

    1. arrow-up-right

      36546.pngarrow-up-right

      5 kB

      Mar/08/22 10:54 AM

    2. arrow-up-right

      187 kB

      1 hour ago

    • Add Linkarrow-up-right

    Issue Links

    is blocked by

    DNT-2794arrow-up-right Add title support to SvgLoader

    • DONE

    Delete this linkarrow-up-right

    relates to

    DNT-2661arrow-up-right Lists must be contained within semantically correct containers

    • REVIEW

    Delete this linkarrow-up-right

    Activity

    • Allarrow-up-right

    • Comments

    • Work Logarrow-up-right

    Permalinkarrow-up-right Editarrow-up-right Deletearrow-up-right

    Guner, Bryanarrow-up-right added a comment - 1 hour ago

    Validated in test environment:

    • Commentarrow-up-right

    People

    Assignee:

    Guner, Bryan

    Reporter:

    Sowa, Bill

    Votes:

    0 arrow-up-rightVote for this issuearrow-up-right

    Watchers:

    2 arrow-up-rightStop watching this issuearrow-up-right

    Dates

    Created:

    Feb/10/22 8:42 AM

    Updated:

    18 minutes ago

    Development

    • 1 brancharrow-up-right

      Updated 6 days ago

    • 5 commitsarrow-up-right

      Latest 5 days ago

    • MERGED

      Updated 7 hours ago

    Agile

    Future Sprint:

    Sprint 14arrow-up-right

    View on Boardarrow-up-right

    Hipchat discussions

    Do you want to discuss this issue? Connect to Hipchat.

    ConnectDismissarrow-up-right

    [3/1 2:28 PM] Macias, Marcie

    home-services/gas-line-repair/enroll

    [3/1 2:42 PM] Macias, Marcie

    home/start-stop-move/stop

    Linked Applicationsarrow-up-right
    arrow-up-right
    Dashboardsarrow-up-right
    /
    and
    /count
    rendering respective components.

    Throttling Example with useEffect

    Suppose we need to subscribe a scroll event on Count component on its mount and just increment the count on every scroll event.

    Code without using throttle or debounce techniques will be like:

    Suppose in practical applications you need to use throttle and wait for every 100ms before we execute increaseCount. I have used the lodash throttle function for this example.

    Wait, no need to hurry. It will work if you are at /count route. The increaseCount function will be throttled and will increase the count after 100ms of intervals.

    But as you move to the / route to render the Home component and unmount the Count component, and start scrolling on home page, you will notice a warning in console which warns about memory leak. This is probably because the scroll event was not cleaned properly. The reason is _.throttle(increaseCount, 100) is called again during unmount and returns another function which does not match that created during the mount stage. What if we create a variable and store the throttled instance.

    like this

    But it has problem too. The throttledCount is created on every render, which is not at all required. This function should be initiated once which is possible inside the useEffect hook. As it will now be computed only once during mount.

    Debounce Example using useCallback or useRef

    Above example is pretty simple. Let's look at another example where there is an input field and you need to increment the count only after user stops typing for certain time. And there is text which is updated on every keystroke which re renders the component on every input.

    Code with debounce:

    This will not work. The count will increase for every keystroke. The reason behind is that on every render, a new debouncedCount is created. We have to store this debounced function such that it is initiated only once like that in useEffect in above example. Here comes use of useCallback. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed - React docs Replace

    with

    and it will work. Because this time the function is evaluated only once at the initial phase.

    Or we can also use useRef by doing this

    One should always keep in mind that every render call of react functional component will lead to expiration of local variables and re-initiation unless you memoize them using hooks.

    export default function App() {
      return (
        <BrowserRouter>
          <div>
            <nav>
              <ul>
                <li>
                  <Link to="/">Home</Link>
                </li>
                <li>
                  <Link to="/count">Count</Link>
                </li>
              </ul>
            </nav>
            <Switch>
    
    function Count() {
      const [count, setCount] = useState(1);
      useEffect(() => {
        window.addEventListener('scroll', increaseCount);
        return () => window.removeEventListener('scroll', increaseCount);
      }, []);
      const increaseCount = () => {
        setCount(count => count + 1);
      }
      return <h2 style={{marginBottom: 1200}}>Count {count}</h2>;
    }
    function Count() {
      const [count, setCount] = useState(1);
      useEffect(() => {
        window.addEventListener('scroll', _.throttle(increaseCount, 100));
        return () => window.removeEventListener('scroll', _.throttle(increaseCount, 100));
      }, []);
      const increaseCount = () => {
        setCount(count => count + 1);
      }
      return <h2 style={{marginBottom: 1200}}>Count {count}</h2>;
    }
    const throttledCount = _.throttle(increaseCount, 100);
    useEffect(() => {
        window.addEventListener('scroll', throttledCount);
        return () => window.removeEventListener('scroll', throttledCount);
      }, []);
    useEffect(() => {
        const throttledCount = _.throttle(increaseCount, 100);
        window.addEventListener('scroll', throttledCount);
        return () => window.removeEventListener('scroll', throttledCount);
      }, []);
    function Count() {
      const [count, setCount] = useState(1);
      const [text, setText] = useState("");
      const increaseCount = () => {
        setCount(count => count + 1);
      }
      const debouncedCount = _.debounce(increaseCount, 1000);
      const handleChange = (e) => {
        setText(e.target.value);
        debouncedCount();
      }
      return <>
        <h2>Count {count}</h2>
        <h3>Text {text}</h3>
    
    
    const debouncedCount = _.debounce(increaseCount, 1000);
    const debouncedCount = useCallback(_.debounce(increaseCount, 1000),[]);
    const debouncedCount = useRef(debounce(increaseCount, 1000)).current;
    <Route path="/count">
    <Count />
    </Route>
    <Route path="/">
    <Home />
    </Route>
    </Switch>
    </div>
    </BrowserRouter>
    );
    }
    <input type="text" onChange={handleChange}></input>
    </>;
    }
    Reportsarrow-up-right
    Issuesarrow-up-right
    Estimatesarrow-up-right
    Componentsarrow-up-right
    Delete this attachmentarrow-up-right
    Screen Shot 2022-03-28 at 4.36.23 PM.pngarrow-up-right
    Historyarrow-up-right
    Activityarrow-up-right
    Time In Statusarrow-up-right
    2 pull requestsarrow-up-right
    Create brancharrow-up-right
    Story - Created by Jira Software - do not edit or delete. Issue type for a user story.
    Medium - Has the potential to affect progress.
    Medium - Has the potential to affect progress.
    Uploaded image for project: 'DXT Ninja Turtles'
    Sub-task - The sub-task of the issue
    BGuner
    BSowa
    Duke Energy JIRA
    User profile for Guner, Bryan
    DXT Ninja Turtles
    Screen Shot 2022-03-28 at 4.36.23 PM.png
    36546.png
    BGuner
    Logo
    Logo
    https://local.duke-energy.com:3000/home-services/gas-line-repair/enrolllocal.duke-energy.comchevron-right
    https://local.duke-energy.com:3000/home-services/gas-line-repair/enrolllocal.duke-energy.comchevron-right
    https://local.duke-energy.com:3000/home-services/gas-line-repair/enrollarrow-up-right
    https://local.duke-energy.com:3000/home/start-stop-move/stoplocal.duke-energy.comchevron-right