Skip to content

Commit 922ae81

Browse files
Matthew Hollowaysynecdokey
Matthew Holloway
authored andcommitted
Assert that uuids and ids are valid HTML ids
1 parent 6e20760 commit 922ae81

10 files changed

+81
-8
lines changed

src/components/AccordionItem.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import DisplayName from '../helpers/DisplayName';
33
import { DivAttributes } from '../helpers/types';
4-
import { nextUuid } from '../helpers/uuid';
4+
import { nextUuid, assertValidHtmlId } from '../helpers/uuid';
55
import {
66
Consumer as ItemConsumer,
77
ItemContext,
@@ -43,6 +43,8 @@ export default class AccordionItem extends React.Component<Props> {
4343
render(): JSX.Element {
4444
const { uuid = this.instanceUuid, dangerouslySetExpanded } = this.props;
4545

46+
if (rest.id) assertValidHtmlId(rest.id);
47+
4648
return (
4749
<ItemProvider
4850
uuid={uuid}

src/components/AccordionItemButton.spec.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import AccordionItemHeading from './AccordionItemHeading';
88
enum UUIDS {
99
FOO = 'FOO',
1010
BAR = 'BAR',
11+
BAD_ID = 'BAD ID',
1112
}
1213

1314
describe('AccordionItem', () => {
@@ -58,6 +59,22 @@ describe('AccordionItem', () => {
5859
});
5960
});
6061

62+
it('throws on invalid uuid', () => {
63+
expect(() => {
64+
render(
65+
<Accordion>
66+
<AccordionItem uuid={UUIDS.BAD_ID}>
67+
<AccordionItemHeading>
68+
<AccordionItemButton>
69+
Hello World
70+
</AccordionItemButton>
71+
</AccordionItemHeading>
72+
</AccordionItem>
73+
</Accordion>,
74+
);
75+
}).toThrow();
76+
});
77+
6178
describe('children prop', () => {
6279
it('is respected', () => {
6380
const { getByText } = render(

src/components/AccordionItemButton.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import keycodes from '../helpers/keycodes';
1111
import { DivAttributes } from '../helpers/types';
1212

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

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

74+
if (rest.id) assertValidHtmlId(rest.id);
75+
7376
return (
7477
<div
7578
{...rest}

src/components/AccordionItemHeading.spec.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import AccordionItemHeading, { SPEC_ERROR } from './AccordionItemHeading';
88
enum UUIDS {
99
FOO = 'FOO',
1010
BAR = 'BAR',
11+
BAD_ID = 'BAD ID',
1112
}
1213

1314
describe('AccordionItem', () => {
@@ -58,6 +59,22 @@ describe('AccordionItem', () => {
5859
});
5960
});
6061

62+
it('throws on invalid uuid', () => {
63+
expect(() => {
64+
render(
65+
<Accordion>
66+
<AccordionItem>
67+
<AccordionItemHeading id={UUIDS.BAD_ID}>
68+
<AccordionItemButton>
69+
Hello World
70+
</AccordionItemButton>
71+
</AccordionItemHeading>
72+
</AccordionItem>
73+
</Accordion>,
74+
);
75+
}).toThrow();
76+
});
77+
6178
describe('children prop', () => {
6279
it('is respected', () => {
6380
const { getByText } = render(

src/components/AccordionItemHeading.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import DisplayName from '../helpers/DisplayName';
44
import { DivAttributes } from '../helpers/types';
55

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

89
type Props = DivAttributes;
910

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

81+
if (props.id) assertValidHtmlId(props.id);
82+
8083
return <AccordionItemHeading {...props} {...headingAttributes} />;
8184
}}
8285
</ItemConsumer>

src/components/AccordionItemPanel.spec.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import AccordionItemPanel from './AccordionItemPanel';
77
enum UUIDS {
88
FOO = 'FOO',
99
BAR = 'BAR',
10+
BAD_ID = 'BAD ID',
1011
}
1112

1213
describe('AccordionItem', () => {
@@ -53,6 +54,18 @@ describe('AccordionItem', () => {
5354
});
5455
});
5556

57+
it('throws on invalid id', () => {
58+
expect(() => {
59+
render(
60+
<Accordion>
61+
<AccordionItem uuid={UUIDS.BAD_ID}>
62+
<AccordionItemPanel id={UUIDS.BAD_ID} />
63+
</AccordionItem>
64+
</Accordion>,
65+
);
66+
}).toThrow();
67+
});
68+
5669
describe('children prop', () => {
5770
it('is respected', () => {
5871
const { getByText } = render(

src/components/AccordionItemPanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import DisplayName from '../helpers/DisplayName';
33
import { DivAttributes } from '../helpers/types';
44
import { Consumer as ItemConsumer, ItemContext } from './ItemContext';
5+
import { assertValidHtmlId } from '../helpers/uuid';
56

67
type Props = DivAttributes;
78

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

1819
renderChildren = ({ panelAttributes }: ItemContext): JSX.Element => {
20+
if (this.props.id) assertValidHtmlId(this.props.id);
21+
1922
return (
2023
<div
2124
data-accordion-component="AccordionItemPanel"

src/components/ItemContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
Consumer as AccordionContextConsumer,
1212
} from './AccordionContext';
1313

14-
export type UUID = string | number;
14+
export type UUID = string;
1515

1616
type ProviderProps = {
1717
children?: React.ReactNode;

src/helpers/uuid.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ import { nextUuid, resetNextUuid } from './uuid';
33
describe('UUID helper', () => {
44
describe('nextUuid', () => {
55
it('generates incremental uuids', () => {
6-
expect(nextUuid()).toBe(0);
7-
expect(nextUuid()).toBe(1);
6+
expect(nextUuid()).toBe('raa-0');
7+
expect(nextUuid()).toBe('raa-1');
88
});
99
});
1010

1111
describe('resetNextUuid', () => {
1212
it('resets the uuid', () => {
1313
resetNextUuid();
14-
expect(nextUuid()).toBe(0);
14+
expect(nextUuid()).toBe('raa-0');
1515
resetNextUuid();
16-
expect(nextUuid()).toBe(0);
16+
expect(nextUuid()).toBe('raa-0');
1717
});
1818
});
1919
});

src/helpers/uuid.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
1+
import { UUID } from '../components/ItemContext';
2+
13
const DEFAULT = 0;
24

35
let counter = DEFAULT;
46

5-
export function nextUuid(): number {
7+
export function nextUuid(): UUID {
68
const current = counter;
79
counter = counter + 1;
810

9-
return current;
11+
return `raa-${current}`;
1012
}
1113

1214
export function resetNextUuid(): void {
1315
counter = DEFAULT;
1416
}
17+
18+
// https://stackoverflow.com/a/14664879
19+
// but modified to allow additional first characters per HTML5
20+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id
21+
const idRegex = /^[_\-.a-zA-Z][\w:.-]*$/;
22+
23+
export function assertValidHtmlId(htmlId: string): void {
24+
if (!htmlId.toString().match(idRegex)) {
25+
throw new Error(
26+
`uuid must be a valid HTML Id but was given "${htmlId}"`,
27+
);
28+
}
29+
}

0 commit comments

Comments
 (0)