Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
498f64c
add logs
aresnik11 Nov 6, 2025
b11d0d6
up timeout
aresnik11 Nov 6, 2025
bb9a2dd
add another log
aresnik11 Nov 6, 2025
c81bae1
try something
aresnik11 Nov 6, 2025
39830e8
longer timeout
aresnik11 Nov 6, 2025
3513a21
change back
aresnik11 Nov 6, 2025
d0e9ab1
try this
aresnik11 Nov 6, 2025
658cc64
fix(InfoTip): wrap focus when shift tabbing
dreamwasp Nov 7, 2025
60c60cc
tests passing, need to dry up
dreamwasp Nov 7, 2025
25abb10
dry up tests
dreamwasp Nov 7, 2025
8e49f83
Children
dreamwasp Nov 7, 2025
c0bb30d
fix
dreamwasp Nov 7, 2025
d4dcb51
Merge branch 'main' into cass-ajr-GMT-1479
dreamwasp Nov 7, 2025
cf339b3
lint
dreamwasp Nov 7, 2025
7de0791
lint fix
dreamwasp Nov 7, 2025
0cd4a2c
Merge branch 'main' into cass-ajr-GMT-1479
dreamwasp Nov 10, 2025
d9ada19
refactors
dreamwasp Nov 12, 2025
1b2d3d0
format
dreamwasp Nov 12, 2025
2781953
fix view
dreamwasp Nov 12, 2025
1204594
DRY up test code
dreamwasp Nov 12, 2025
c159886
Merge branch 'main' into cass-ajr-GMT-1479
dreamwasp Nov 17, 2025
5e68141
test polling
dreamwasp Nov 17, 2025
ba6f9a7
added better focus mgmt + fixed esc handler for inline
dreamwasp Nov 17, 2025
53b3d5f
better docs
dreamwasp Nov 18, 2025
bede696
fix popover r/l width
dreamwasp Nov 18, 2025
259a63b
refactor for container refs
dreamwasp Nov 19, 2025
b00417e
fix(InfoTip): Remove ariaa-live and implement focus mgmt
dreamwasp Nov 20, 2025
a42c7db
remove aria-live
dreamwasp Nov 20, 2025
e0cd55d
implement automatic programmatic fgocus
dreamwasp Nov 20, 2025
1b895ef
add custom label + stories
dreamwasp Nov 20, 2025
8274416
move focusable selectors func
dreamwasp Nov 21, 2025
88cacce
start docs feed back
dreamwasp Nov 21, 2025
0b6be28
clean up docs to detect new behavior
dreamwasp Nov 21, 2025
3fd6067
format
dreamwasp Nov 21, 2025
58800e1
merge base
dreamwasp Nov 21, 2025
17b9269
finish merge + fix docs
dreamwasp Nov 21, 2025
70f5282
fixed onClick + more tests
dreamwasp Nov 21, 2025
3849c68
merge in onClick
dreamwasp Nov 21, 2025
e078377
more focusable list
dreamwasp Nov 21, 2025
0aa3a42
aria-roledescription
dreamwasp Nov 24, 2025
9cc5b0f
test clean up
dreamwasp Nov 25, 2025
ae927ea
clean up
dreamwasp Nov 25, 2025
fc5e360
more cleanup
dreamwasp Nov 25, 2025
d9b132e
merge in test refactors
dreamwasp Nov 25, 2025
ad95c33
start labelledBy
dreamwasp Nov 25, 2025
54bb2d3
aria-label example
dreamwasp Nov 26, 2025
ad0ffba
fix infotops inside modals
dreamwasp Dec 1, 2025
2fcb873
clean up stories
dreamwasp Dec 1, 2025
caaad67
close all on esc
dreamwasp Dec 1, 2025
624041a
add
dreamwasp Dec 1, 2025
8dc7bf7
tests fixed
dreamwasp Dec 1, 2025
3a6c659
Merge branch 'main' into cass-ajr-GMT-1479
dreamwasp Dec 1, 2025
c149f00
refactor tests
dreamwasp Dec 2, 2025
eeefb11
Merge branch 'main' into cass-ajr-GMT-1479
dreamwasp Dec 2, 2025
e0683f5
fix tests
dreamwasp Dec 2, 2025
1b6d841
merge conflicts
dreamwasp Dec 2, 2025
08e244e
remove field label reference
dreamwasp Dec 2, 2025
bbb712d
Merge remote-tracking branch 'origin/cass-ajr-GMT-1479' into cass-GMT…
dreamwasp Dec 2, 2025
0b11c4e
refactor stories for best practices
dreamwasp Dec 2, 2025
6af94df
formatted
dreamwasp Dec 2, 2025
cf5edf9
need to confirm merge changes
dreamwasp Dec 4, 2025
4647bcb
put selector back
dreamwasp Dec 5, 2025
5e8ee27
start infotip form accessibility
dreamwasp Dec 5, 2025
aabf026
Merge branch 'main' into cass-GMT-216
dreamwasp Dec 5, 2025
cadb7f4
add docs to form infotips
dreamwasp Dec 5, 2025
aa36d99
Merge branch 'main' into cass-GMT-216
dreamwasp Dec 8, 2025
08c5459
needless comment
dreamwasp Dec 9, 2025
f80df1d
test token
dreamwasp Dec 9, 2025
d1626f1
publish
dreamwasp Dec 9, 2025
0a823ee
Merge branch 'main' into cass-GMT-216
dreamwasp Dec 10, 2025
d860310
More information microcopy
dreamwasp Dec 10, 2025
8dd47a3
add tests
dreamwasp Dec 10, 2025
ef78c2c
Merge branch 'main' into cass-GMT-216
dreamwasp Dec 17, 2025
f8f5412
Merge branch 'main' into cass-GMT-216
dreamwasp Jan 6, 2026
d22d5c2
amy commments
dreamwasp Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getJestProjects } from '@nx/jest';
import { getJestProjectsAsync } from '@nx/jest';

export default {
projects: getJestProjects(),
};
export default async () => ({
projects: await getJestProjectsAsync(),
});
5 changes: 5 additions & 0 deletions packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export interface ConnectedFormGroupBaseProps
name: string;
label: React.ReactNode;
required?: boolean;
/**
* InfoTip to display next to the field label. String labels automatically
* label the InfoTip button. For ReactNode labels, provide `ariaLabel` or
* set `labelledByFieldLabel: true` to ensure the InfoTip is accessible.
*/
infotip?: InfoTipProps;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { setupRtl } from '@codecademy/gamut-tests';
import { fireEvent, queries } from '@testing-library/dom';
import { act, RenderResult, waitFor } from '@testing-library/react';

import { InfoTipProps } from '../../Tip/InfoTip';
import { createPromise } from '../../utils';
import { ConnectedForm } from '..';
import { ConnectedForm, ConnectedFormGroup, ConnectedInput } from '..';
import { PlainConnectedFields } from '../__fixtures__/helpers';

const renderView = setupRtl(ConnectedForm, {
Expand Down Expand Up @@ -359,3 +360,87 @@ describe('ConnectedForm', () => {
});
});
});

describe('ConnectedFormGroup infotip accessibility', () => {
const label = 'Infotip Label';
const info = 'helpful information';
const ariaLabel = 'Custom label';

const renderConnectedFormGroupView = setupRtl(ConnectedForm, {
defaultValues: { input: '' },
onSubmit: jest.fn(),
children: null,
});

const renderWithInfotip = ({
infotip,
fieldLabel = label,
}: {
infotip: InfoTipProps;
fieldLabel?: React.ReactNode;
}) =>
renderConnectedFormGroupView({
children: (
<ConnectedFormGroup
field={{ component: ConnectedInput }}
infotip={infotip}
label={fieldLabel}
name="input"
/>
),
});

it('automatically labels InfoTip button by the field label when label is a string', () => {
const { view } = renderWithInfotip({ infotip: { info } });

view.getByRole('button', { name: `${label}\u00A0(optional)` });
});

it('uses explicit ariaLabel when provided', () => {
const { view } = renderWithInfotip({
infotip: { info, ariaLabel },
});

view.getByRole('button', { name: ariaLabel });
});

it('uses explicit ariaLabelledby when provided', () => {
const externalLabelId = 'external-label-id';
const externalLabelText = 'External Label';

const { view } = renderConnectedFormGroupView({
children: (
<>
<span id={externalLabelId}>{externalLabelText}</span>
<ConnectedFormGroup
field={{ component: ConnectedInput }}
infotip={{ info, ariaLabelledby: externalLabelId }}
label={label}
name="input"
/>
</>
),
});

view.getByRole('button', { name: externalLabelText });
});

it('does not automatically label InfoTip when label is a ReactNode', () => {
const { view } = renderWithInfotip({
infotip: { info, ariaLabel },
fieldLabel: <span>{label}</span>,
});

view.getByRole('button', { name: ariaLabel });
expect(view.queryByRole('button', { name: new RegExp(label) })).toBeNull();
});

it('labels InfoTip by field label when labelledByFieldLabel is true with ReactNode label', () => {
const { view } = renderWithInfotip({
infotip: { info, labelledByFieldLabel: true },
fieldLabel: <span>{label}</span>,
});

view.getByRole('button', { name: new RegExp(label) });
});
});
40 changes: 40 additions & 0 deletions packages/gamut/src/Form/__tests__/FormGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,44 @@ describe('FormGroup', () => {
'there is no up dog here...'
);
});

describe('infotip accessibility', () => {
const info = 'helpful information';
const ariaLabel = 'Custom label';

it('automatically labels InfoTip button by the field label when label is a string', () => {
const { view } = renderView({ label, htmlFor, infotip: { info } });

view.getByRole('button', { name: new RegExp(label) });
});

it('uses explicit ariaLabel when provided', () => {
const { view } = renderView({
label,
htmlFor,
infotip: { info, ariaLabel },
});

view.getByRole('button', { name: ariaLabel });
});

it('uses explicit ariaLabelledby when provided', () => {
const externalLabelId = 'external-label-id';
const externalLabelText = 'External Label';

const { view } = renderView({
label,
htmlFor,
infotip: { info, ariaLabelledby: externalLabelId },
children: (
<>
<span id={externalLabelId}>{externalLabelText}</span>
<Input id={htmlFor} />
</>
),
});

view.getByRole('button', { name: externalLabelText });
});
});
});
17 changes: 15 additions & 2 deletions packages/gamut/src/Form/elements/FormGroupLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { states, variant } from '@codecademy/gamut-styles';
import { StyleProps } from '@codecademy/variance';
import styled from '@emotion/styled';
import { HTMLAttributes } from 'react';
import { HTMLAttributes, useId } from 'react';
import * as React from 'react';

import { FlexBox } from '../..';
Expand Down Expand Up @@ -62,6 +62,13 @@ export const FormGroupLabel: React.FC<FormGroupLabelProps> = ({
size,
...rest
}) => {
const labelId = useId();
const isStringLabel = typeof children === 'string';
const shouldLabelInfoTip =
(isStringLabel || infotip?.labelledByFieldLabel) &&
!infotip?.ariaLabel &&
!infotip?.ariaLabelledby;

return (
<FlexBox justifyContent="space-between" mb={4}>
<Label
Expand All @@ -70,6 +77,7 @@ export const FormGroupLabel: React.FC<FormGroupLabelProps> = ({
className={className}
disabled={disabled}
htmlFor={htmlFor}
id={infotip && shouldLabelInfoTip ? labelId : undefined}
size={size}
>
{children}
Expand All @@ -82,7 +90,12 @@ export const FormGroupLabel: React.FC<FormGroupLabelProps> = ({
'\u00A0(optional)'
))}
</Label>
{infotip && <InfoTip {...infotip} />}
{infotip && (
<InfoTip
{...infotip}
ariaLabelledby={shouldLabelInfoTip ? labelId : infotip.ariaLabelledby}
/>
)}
</FlexBox>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,15 @@ export const getComponent = (componentName: string) => {

type GridFormInputGroupTestComponentProps = GridFormInputGroupProps & {
mode?: 'onSubmit' | 'onChange';
externalLabel?: { id: string; text: string };
};

export const GridFormInputGroupTestComponent: React.FC<
GridFormInputGroupTestComponentProps
> = ({ field, mode = 'onSubmit', ...rest }) => {
> = ({ field, mode = 'onSubmit', externalLabel, ...rest }) => {
return (
<FormContext mode={mode}>
{externalLabel && <span id={externalLabel.id}>{externalLabel.text}</span>}
<GridFormInputGroup field={field} {...rest} />
</FormContext>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,69 @@ describe('GridFormInputGroup', () => {
});
expect(view.container).not.toContainHTML('Column');
});

describe('infotip accessibility', () => {
const info = 'helpful information';
const ariaLabel = 'Custom label';
const textLabel = 'Stub Text';
const checkboxLabel = 'Check me!';

it('automatically labels InfoTip button by the field label when label is a string', () => {
const { view } = renderView({
field: { ...stubTextField, infotip: { info } },
});

view.getByRole('button', { name: new RegExp(textLabel) });
});

it('uses explicit ariaLabel when provided', () => {
const { view } = renderView({
field: { ...stubTextField, infotip: { info, ariaLabel } },
});

view.getByRole('button', { name: ariaLabel });
});

it('uses explicit ariaLabelledby when provided', () => {
const externalLabelId = 'external-label-id';
const externalLabelText = 'External Label';

const { view } = renderView({
field: {
...stubTextField,
infotip: { info, ariaLabelledby: externalLabelId },
},
externalLabel: { id: externalLabelId, text: externalLabelText },
});

view.getByRole('button', { name: externalLabelText });
});

it('does not automatically label InfoTip when label is a ReactNode', () => {
const { view } = renderView({
field: {
...stubCheckboxField,
label: <span>{checkboxLabel}</span>,
infotip: { info, ariaLabel },
},
});

view.getByRole('button', { name: ariaLabel });
expect(
view.queryByRole('button', { name: new RegExp(checkboxLabel) })
).toBeNull();
});

it('labels InfoTip by field label when labelledByFieldLabel is true with ReactNode label', () => {
const { view } = renderView({
field: {
...stubCheckboxField,
label: <span>{checkboxLabel}</span>,
infotip: { info, labelledByFieldLabel: true },
},
});

view.getByRole('button', { name: new RegExp(checkboxLabel) });
});
});
});
5 changes: 5 additions & 0 deletions packages/gamut/src/GridForm/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export type BaseFormField<Value> = {
*/
id?: string;

/**
* InfoTip to display next to the field label. String labels automatically
* label the InfoTip button. For ReactNode labels, provide `ariaLabel` or
* set `labelledByFieldLabel: true` to ensure the InfoTip is accessible.
*/
infotip?: InfoTipProps;

isSoloField?: boolean;
Expand Down
17 changes: 14 additions & 3 deletions packages/gamut/src/Tip/InfoTip/InfoTipButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,26 @@ export type InfoTipButtonProps = ComponentProps<typeof InfoTipButtonBase> &
Pick<InfoTipProps, 'emphasis'>;

export const InfoTipButton = forwardRef<ButtonBaseElements, InfoTipButtonProps>(
({ active, children, emphasis, ...props }, ref) => {
(
{
active,
children,
emphasis,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledby,
...props
},
ref
) => {
const Icon = emphasis === 'high' ? MiniInfoCircleIcon : MiniInfoOutlineIcon;

return (
<InfoTipButtonBase
{...props}
active={active}
aria-expanded={active}
aria-label="Show information"
{...props}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
ref={ref}
>
{Icon && (
Expand Down
8 changes: 0 additions & 8 deletions packages/gamut/src/Tip/InfoTip/elements.tsx

This file was deleted.

Loading