Skip to content

Commit

Permalink
Assert that uuids and ids are valid HTML ids
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Holloway committed Jun 3, 2020
1 parent 558510c commit b51cb4a
Show file tree
Hide file tree
Showing 10 changed files with 81 additions and 8 deletions.
4 changes: 3 additions & 1 deletion src/components/AccordionItem.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import DisplayName from '../helpers/DisplayName';
import { DivAttributes } from '../helpers/types';
import { nextUuid } from '../helpers/uuid';
import { nextUuid, assertValidHtmlId } from '../helpers/uuid';
import { Provider as ItemProvider, UUID } from './ItemContext';

type Props = DivAttributes & {
Expand All @@ -22,6 +22,8 @@ export default class AccordionItem extends React.Component<Props> {
render(): JSX.Element {
const { uuid = this.instanceUuid, ...rest } = this.props;

if (rest.id) assertValidHtmlId(rest.id);

return (
<ItemProvider uuid={uuid}>
<div data-accordion-component="AccordionItem" {...rest} />
Expand Down
17 changes: 17 additions & 0 deletions src/components/AccordionItemButton.spec.tsx
Expand Up @@ -8,6 +8,7 @@ import AccordionItemHeading from './AccordionItemHeading';
enum UUIDS {
FOO = 'FOO',
BAR = 'BAR',
BAD_ID = 'BAD ID',
}

describe('AccordionItem', () => {
Expand Down Expand Up @@ -58,6 +59,22 @@ describe('AccordionItem', () => {
});
});

it('throws on invalid uuid', () => {
expect(() => {
render(
<Accordion>
<AccordionItem uuid={UUIDS.BAD_ID}>
<AccordionItemHeading>
<AccordionItemButton>
Hello World
</AccordionItemButton>
</AccordionItemHeading>
</AccordionItem>
</Accordion>,
);
}).toThrow();
});

describe('children prop', () => {
it('is respected', () => {
const { getByText } = render(
Expand Down
3 changes: 3 additions & 0 deletions src/components/AccordionItemButton.tsx
Expand Up @@ -11,6 +11,7 @@ import keycodes from '../helpers/keycodes';
import { DivAttributes } from '../helpers/types';

import { Consumer as ItemConsumer, ItemContext } from './ItemContext';
import { assertValidHtmlId } from '../helpers/uuid';

type Props = DivAttributes & {
toggleExpanded(): void;
Expand Down Expand Up @@ -70,6 +71,8 @@ export class AccordionItemButton extends React.PureComponent<Props> {
render(): JSX.Element {
const { toggleExpanded, ...rest } = this.props;

if (rest.id) assertValidHtmlId(rest.id);

return (
<div
{...rest}
Expand Down
17 changes: 17 additions & 0 deletions src/components/AccordionItemHeading.spec.tsx
Expand Up @@ -8,6 +8,7 @@ import AccordionItemHeading, { SPEC_ERROR } from './AccordionItemHeading';
enum UUIDS {
FOO = 'FOO',
BAR = 'BAR',
BAD_ID = 'BAD ID',
}

describe('AccordionItem', () => {
Expand Down Expand Up @@ -58,6 +59,22 @@ describe('AccordionItem', () => {
});
});

it('throws on invalid uuid', () => {
expect(() => {
render(
<Accordion>
<AccordionItem>
<AccordionItemHeading id={UUIDS.BAD_ID}>
<AccordionItemButton>
Hello World
</AccordionItemButton>
</AccordionItemHeading>
</AccordionItem>
</Accordion>,
);
}).toThrow();
});

describe('children prop', () => {
it('is respected', () => {
const { getByText } = render(
Expand Down
3 changes: 3 additions & 0 deletions src/components/AccordionItemHeading.tsx
Expand Up @@ -4,6 +4,7 @@ import DisplayName from '../helpers/DisplayName';
import { DivAttributes } from '../helpers/types';

import { Consumer as ItemConsumer, ItemContext } from './ItemContext';
import { assertValidHtmlId } from '../helpers/uuid';

type Props = DivAttributes;

Expand Down Expand Up @@ -77,6 +78,8 @@ const AccordionItemHeadingWrapper: React.SFC<DivAttributes> = (
{(itemContext: ItemContext): JSX.Element => {
const { headingAttributes } = itemContext;

if (props.id) assertValidHtmlId(props.id);

return <AccordionItemHeading {...props} {...headingAttributes} />;
}}
</ItemConsumer>
Expand Down
13 changes: 13 additions & 0 deletions src/components/AccordionItemPanel.spec.tsx
Expand Up @@ -7,6 +7,7 @@ import AccordionItemPanel from './AccordionItemPanel';
enum UUIDS {
FOO = 'FOO',
BAR = 'BAR',
BAD_ID = 'BAD ID',
}

describe('AccordionItem', () => {
Expand Down Expand Up @@ -53,6 +54,18 @@ describe('AccordionItem', () => {
});
});

it('throws on invalid id', () => {
expect(() => {
render(
<Accordion>
<AccordionItem uuid={UUIDS.BAD_ID}>
<AccordionItemPanel id={UUIDS.BAD_ID} />
</AccordionItem>
</Accordion>,
);
}).toThrow();
});

describe('children prop', () => {
it('is respected', () => {
const { getByText } = render(
Expand Down
3 changes: 3 additions & 0 deletions src/components/AccordionItemPanel.tsx
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import DisplayName from '../helpers/DisplayName';
import { DivAttributes } from '../helpers/types';
import { Consumer as ItemConsumer, ItemContext } from './ItemContext';
import { assertValidHtmlId } from '../helpers/uuid';

type Props = DivAttributes;

Expand All @@ -16,6 +17,8 @@ export default class AccordionItemPanel extends React.Component<Props> {
DisplayName.AccordionItemPanel;

renderChildren = ({ panelAttributes }: ItemContext): JSX.Element => {
if (this.props.id) assertValidHtmlId(this.props.id);

return (
<div
data-accordion-component="AccordionItemPanel"
Expand Down
2 changes: 1 addition & 1 deletion src/components/ItemContext.tsx
Expand Up @@ -11,7 +11,7 @@ import {
Consumer as AccordionContextConsumer,
} from './AccordionContext';

export type UUID = string | number;
export type UUID = string;

type ProviderProps = {
children?: React.ReactNode;
Expand Down
8 changes: 4 additions & 4 deletions src/helpers/uuid.spec.ts
Expand Up @@ -3,17 +3,17 @@ import { nextUuid, resetNextUuid } from './uuid';
describe('UUID helper', () => {
describe('nextUuid', () => {
it('generates incremental uuids', () => {
expect(nextUuid()).toBe(0);
expect(nextUuid()).toBe(1);
expect(nextUuid()).toBe('raa-0');
expect(nextUuid()).toBe('raa-1');
});
});

describe('resetNextUuid', () => {
it('resets the uuid', () => {
resetNextUuid();
expect(nextUuid()).toBe(0);
expect(nextUuid()).toBe('raa-0');
resetNextUuid();
expect(nextUuid()).toBe(0);
expect(nextUuid()).toBe('raa-0');
});
});
});
19 changes: 17 additions & 2 deletions src/helpers/uuid.ts
@@ -1,14 +1,29 @@
import { UUID } from '../components/ItemContext';

const DEFAULT = 0;

let counter = DEFAULT;

export function nextUuid(): number {
export function nextUuid(): UUID {
const current = counter;
counter = counter + 1;

return current;
return `raa-${current}`;
}

export function resetNextUuid(): void {
counter = DEFAULT;
}

// https://stackoverflow.com/a/14664879
// but modified to allow additional first characters per HTML5
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id
const idRegex = /^[_\-.a-zA-Z][\w:.-]*$/;

export function assertValidHtmlId(htmlId: string): void {
if (!htmlId.toString().match(idRegex)) {
throw new Error(
`uuid must be a valid HTML Id but was given "${htmlId}"`,
);
}
}

0 comments on commit b51cb4a

Please sign in to comment.