Skip to content

Commit 38c8e57

Browse files
author
Matthew Holloway
authored
Merge pull request #265 from springload/feature/assert-ids-uuids
Assert that uuids and ids are valid HTML ids
2 parents 6e20760 + 624d59b commit 38c8e57

10 files changed

+67
-10
lines changed

src/components/AccordionItem.tsx

Lines changed: 10 additions & 2 deletions
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 { assertValidHtmlId, nextUuid } from '../helpers/uuid';
55
import {
66
Consumer as ItemConsumer,
77
ItemContext,
@@ -41,7 +41,15 @@ export default class AccordionItem extends React.Component<Props> {
4141
};
4242

4343
render(): JSX.Element {
44-
const { uuid = this.instanceUuid, dangerouslySetExpanded } = this.props;
44+
const {
45+
uuid = this.instanceUuid,
46+
dangerouslySetExpanded,
47+
...rest
48+
} = this.props;
49+
50+
if (rest.id) {
51+
assertValidHtmlId(rest.id);
52+
}
4553

4654
return (
4755
<ItemProvider

src/components/AccordionItemButton.spec.tsx

Lines changed: 1 addition & 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', () => {

src/components/AccordionItemButton.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '../helpers/focus';
1010
import keycodes from '../helpers/keycodes';
1111
import { DivAttributes } from '../helpers/types';
12+
import { assertValidHtmlId } from '../helpers/uuid';
1213

1314
import { Consumer as ItemConsumer, ItemContext } from './ItemContext';
1415

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

74+
if (rest.id) {
75+
assertValidHtmlId(rest.id);
76+
}
77+
7378
return (
7479
<div
7580
{...rest}

src/components/AccordionItemHeading.spec.tsx

Lines changed: 1 addition & 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', () => {

src/components/AccordionItemHeading.tsx

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

67
import { Consumer as ItemConsumer, ItemContext } from './ItemContext';
78

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

81+
if (props.id) {
82+
assertValidHtmlId(props.id);
83+
}
84+
8085
return <AccordionItemHeading {...props} {...headingAttributes} />;
8186
}}
8287
</ItemConsumer>

src/components/AccordionItemPanel.spec.tsx

Lines changed: 1 addition & 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', () => {

src/components/AccordionItemPanel.tsx

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

67
type Props = DivAttributes;
@@ -16,6 +17,10 @@ export default class AccordionItemPanel extends React.Component<Props> {
1617
DisplayName.AccordionItemPanel;
1718

1819
renderChildren = ({ panelAttributes }: ItemContext): JSX.Element => {
20+
if (this.props.id) {
21+
assertValidHtmlId(this.props.id);
22+
}
23+
1924
return (
2025
<div
2126
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: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
1-
import { nextUuid, resetNextUuid } from './uuid';
1+
import { assertValidHtmlId, nextUuid, resetNextUuid } from './uuid';
22

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');
17+
});
18+
});
19+
20+
describe('assertValidHtmlId', () => {
21+
it("returns false in case there's a whitespace or an empty string", () => {
22+
expect(assertValidHtmlId('a a')).toBe(false);
23+
expect(assertValidHtmlId('')).toBe(false);
24+
});
25+
26+
it('returns true on a valid id', () => {
27+
expect(assertValidHtmlId('💜')).toBe(true);
28+
expect(assertValidHtmlId('✅')).toBe(true);
1729
});
1830
});
1931
});

src/helpers/uuid.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
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+
// HTML5 ids allow all unicode characters, except for ASCII whitespaces
19+
// https://infra.spec.whatwg.org/#ascii-whitespace
20+
const idRegex = /[\u0009\u000a\u000c\u000d\u0020]/g;
21+
22+
export function assertValidHtmlId(htmlId: string): boolean {
23+
if (htmlId === '' || idRegex.test(htmlId)) {
24+
// tslint:disable-next-line
25+
console.error(
26+
`uuid must be a valid HTML5 id but was given "${htmlId}", ASCII whitespaces are forbidden`,
27+
);
28+
29+
return false;
30+
}
31+
32+
return true;
33+
}

0 commit comments

Comments
 (0)