Skip to content

Commit

Permalink
fix(js): rely on environment instead of global object (#572)
Browse files Browse the repository at this point in the history
* fix(js): rely on `environment` instead of global object (#572)

* chore(eslint): disallow `window` and `document` usage

Co-authored-by: François Chalifour <francoischalifour@users.noreply.github.com>
  • Loading branch information
shortcuts and francoischalifour authored May 6, 2021
1 parent f2154c8 commit 0a33b44
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 49 deletions.
25 changes: 25 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,31 @@ module.exports = {
'eslint-comments/no-unlimited-disable': OFF,
},
},
{
files: [
'packages/autocomplete-core/**/*',
'packages/autocomplete-js/**/*',
],
rules: {
'no-restricted-globals': [
'error',
{
name: 'window',
message: 'Use the `environment` param to access this property.',
},
{
name: 'document',
message: 'Use the `environment` param to access this property.',
},
],
},
},
{
files: ['**/__tests__/**'],
rules: {
'no-restricted-globals': OFF,
},
},
{
files: ['**/rollup.config.js', 'stories/**/*', '**/__tests__/**'],
rules: {
Expand Down
2 changes: 1 addition & 1 deletion bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
{
"path": "packages/autocomplete-js/dist/umd/index.production.js",
"maxSize": "15.25 kB"
"maxSize": "15.5 kB"
},
{
"path": "packages/autocomplete-preset-algolia/dist/umd/index.production.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/autocomplete-core/src/getDefaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ export function getDefaultProps<TItem extends BaseItem>(
props: AutocompleteOptions<TItem>,
pluginSubscribers: AutocompleteSubscribers<TItem>
): InternalAutocompleteOptions<TItem> {
/* eslint-disable no-restricted-globals */
const environment: AutocompleteEnvironment = (typeof window !== 'undefined'
? window
: {}) as typeof window;
/* eslint-enable no-restricted-globals */
const plugins = props.plugins || [];

return {
Expand Down
21 changes: 15 additions & 6 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function autocomplete<TItem extends BaseItem>(
autocomplete: autocomplete.value,
autocompleteScopeApi,
classNames: props.value.renderer.classNames,
environment: props.value.core.environment,
isDetached: isDetached.value,
placeholder: props.value.core.placeholder,
propGetters,
Expand Down Expand Up @@ -237,7 +238,7 @@ export function autocomplete<TItem extends BaseItem>(
// We scroll to the top of the panel whenever the query changes (i.e. new
// results come in) so that users don't have to.
if (state.query !== prevState.query) {
const scrollablePanels = document.querySelectorAll(
const scrollablePanels = props.value.core.environment.document.querySelectorAll(
'.aa-Panel--scrollable'
);
scrollablePanels.forEach((scrollablePanel) => {
Expand Down Expand Up @@ -336,19 +337,27 @@ export function autocomplete<TItem extends BaseItem>(

function setIsModalOpen(value: boolean) {
requestAnimationFrame(() => {
const prevValue = document.body.contains(dom.value.detachedOverlay);
const prevValue = props.value.core.environment.document.body.contains(
dom.value.detachedOverlay
);

if (value === prevValue) {
return;
}

if (value) {
document.body.appendChild(dom.value.detachedOverlay);
document.body.classList.add('aa-Detached');
props.value.core.environment.document.body.appendChild(
dom.value.detachedOverlay
);
props.value.core.environment.document.body.classList.add('aa-Detached');
dom.value.input.focus();
} else {
document.body.removeChild(dom.value.detachedOverlay);
document.body.classList.remove('aa-Detached');
props.value.core.environment.document.body.removeChild(
dom.value.detachedOverlay
);
props.value.core.environment.document.body.classList.remove(
'aa-Detached'
);
autocomplete.value.setQuery('');
autocomplete.value.refresh();
}
Expand Down
16 changes: 11 additions & 5 deletions packages/autocomplete-js/src/createAutocompleteDom.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
AutocompleteApi as AutocompleteCoreApi,
AutocompleteEnvironment,
AutocompleteScopeApi,
BaseItem,
} from '@algolia/autocomplete-core';

import { createDomElement } from './createDomElement';
import { ClearIcon, Input, LoadingIcon, SearchIcon } from './elements';
import { getCreateDomElement } from './getCreateDomElement';
import {
AutocompleteClassNames,
AutocompleteDom,
Expand All @@ -18,6 +19,7 @@ type CreateDomProps<TItem extends BaseItem> = {
autocomplete: AutocompleteCoreApi<TItem>;
autocompleteScopeApi: AutocompleteScopeApi<TItem>;
classNames: AutocompleteClassNames;
environment: AutocompleteEnvironment;
isDetached: boolean;
placeholder?: string;
propGetters: AutocompletePropGetters<TItem>;
Expand All @@ -29,12 +31,15 @@ export function createAutocompleteDom<TItem extends BaseItem>({
autocomplete,
autocompleteScopeApi,
classNames,
environment,
isDetached,
placeholder = 'Search',
propGetters,
setIsModalOpen,
state,
}: CreateDomProps<TItem>): AutocompleteDom {
const createDomElement = getCreateDomElement(environment);

const rootProps = propGetters.getRootProps({
state,
props: autocomplete.getRootProps({}),
Expand Down Expand Up @@ -68,7 +73,7 @@ export function createAutocompleteDom<TItem extends BaseItem>({
class: classNames.submitButton,
type: 'submit',
title: 'Submit',
children: [SearchIcon({})],
children: [SearchIcon({ environment })],
});
const label = createDomElement('label', {
class: classNames.label,
Expand All @@ -79,15 +84,16 @@ export function createAutocompleteDom<TItem extends BaseItem>({
class: classNames.clearButton,
type: 'reset',
title: 'Clear',
children: [ClearIcon({})],
children: [ClearIcon({ environment })],
});
const loadingIndicator = createDomElement('div', {
class: classNames.loadingIndicator,
children: [LoadingIcon({})],
children: [LoadingIcon({ environment })],
});

const input = Input({
class: classNames.input,
environment,
state,
getInputProps: propGetters.getInputProps,
getInputPropsCore: autocomplete.getInputProps,
Expand Down Expand Up @@ -142,7 +148,7 @@ export function createAutocompleteDom<TItem extends BaseItem>({
if (isDetached) {
const detachedSearchButtonIcon = createDomElement('div', {
class: classNames.detachedSearchButtonIcon,
children: [SearchIcon({})],
children: [SearchIcon({ environment })],
});
const detachedSearchButtonPlaceholder = createDomElement('div', {
class: classNames.detachedSearchButtonPlaceholder,
Expand Down
16 changes: 0 additions & 16 deletions packages/autocomplete-js/src/createDomElement.ts

This file was deleted.

17 changes: 14 additions & 3 deletions packages/autocomplete-js/src/elements/ClearIcon.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { AutocompleteEnvironment } from '@algolia/autocomplete-core';

import { AutocompleteElement } from '../types/AutocompleteElement';

export const ClearIcon: AutocompleteElement<{}, SVGSVGElement> = () => {
const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
export const ClearIcon: AutocompleteElement<
{ environment: AutocompleteEnvironment },
SVGSVGElement
> = ({ environment }) => {
const element = environment.document.createElementNS(
'http://www.w3.org/2000/svg',
'svg'
);
element.setAttribute('class', 'aa-ClearIcon');
element.setAttribute('viewBox', '0 0 24 24');
element.setAttribute('width', '18');
element.setAttribute('height', '18');
element.setAttribute('fill', 'currentColor');

const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const path = environment.document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute(
'd',
'M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z'
Expand Down
6 changes: 5 additions & 1 deletion packages/autocomplete-js/src/elements/Input.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import {
AutocompleteApi as AutocompleteCoreApi,
AutocompleteEnvironment,
AutocompleteScopeApi,
} from '@algolia/autocomplete-core';

import { createDomElement } from '../createDomElement';
import { getCreateDomElement } from '../getCreateDomElement';
import { AutocompletePropGetters, AutocompleteState } from '../types';
import { AutocompleteElement } from '../types/AutocompleteElement';
import { setProperties } from '../utils';

type InputProps = {
autocompleteScopeApi: AutocompleteScopeApi<any>;
environment: AutocompleteEnvironment;
getInputProps: AutocompletePropGetters<any>['getInputProps'];
getInputPropsCore: AutocompleteCoreApi<any>['getInputProps'];
onDetachedEscape?(): void;
Expand All @@ -18,13 +20,15 @@ type InputProps = {

export const Input: AutocompleteElement<InputProps, HTMLInputElement> = ({
autocompleteScopeApi,
environment,
classNames,
getInputProps,
getInputPropsCore,
onDetachedEscape,
state,
...props
}) => {
const createDomElement = getCreateDomElement(environment);
const element = createDomElement('input', props);
const inputProps = getInputProps({
state,
Expand Down
12 changes: 10 additions & 2 deletions packages/autocomplete-js/src/elements/LoadingIcon.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { AutocompleteEnvironment } from '@algolia/autocomplete-core';

import { AutocompleteElement } from '../types/AutocompleteElement';

export const LoadingIcon: AutocompleteElement<{}, SVGSVGElement> = () => {
const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
export const LoadingIcon: AutocompleteElement<
{ environment: AutocompleteEnvironment },
SVGSVGElement
> = ({ environment }) => {
const element = environment.document.createElementNS(
'http://www.w3.org/2000/svg',
'svg'
);
element.setAttribute('class', 'aa-LoadingIcon');
element.setAttribute('viewBox', '0 0 100 100');
element.setAttribute('width', '20');
Expand Down
17 changes: 14 additions & 3 deletions packages/autocomplete-js/src/elements/SearchIcon.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { AutocompleteEnvironment } from '@algolia/autocomplete-core';

import { AutocompleteElement } from '../types/AutocompleteElement';

export const SearchIcon: AutocompleteElement<{}, SVGSVGElement> = () => {
const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
export const SearchIcon: AutocompleteElement<
{ environment: AutocompleteEnvironment },
SVGSVGElement
> = ({ environment }) => {
const element = environment.document.createElementNS(
'http://www.w3.org/2000/svg',
'svg'
);
element.setAttribute('class', 'aa-SubmitIcon');
element.setAttribute('viewBox', '0 0 24 24');
element.setAttribute('width', '20');
element.setAttribute('height', '20');
element.setAttribute('fill', 'currentColor');

const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const path = environment.document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
path.setAttribute(
'd',
'M16.041 15.856c-0.034 0.026-0.067 0.055-0.099 0.087s-0.060 0.064-0.087 0.099c-1.258 1.213-2.969 1.958-4.855 1.958-1.933 0-3.682-0.782-4.95-2.050s-2.050-3.017-2.050-4.95 0.782-3.682 2.050-4.95 3.017-2.050 4.95-2.050 3.682 0.782 4.95 2.050 2.050 3.017 2.050 4.95c0 1.886-0.745 3.597-1.959 4.856zM21.707 20.293l-3.675-3.675c1.231-1.54 1.968-3.493 1.968-5.618 0-2.485-1.008-4.736-2.636-6.364s-3.879-2.636-6.364-2.636-4.736 1.008-6.364 2.636-2.636 3.879-2.636 6.364 1.008 4.736 2.636 6.364 3.879 2.636 6.364 2.636c2.125 0 4.078-0.737 5.618-1.968l3.675 3.675c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414z'
Expand Down
20 changes: 20 additions & 0 deletions packages/autocomplete-js/src/getCreateDomElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { AutocompleteEnvironment } from '@algolia/autocomplete-core';

import { setProperties } from './utils';

type CreateDomElementProps = Record<string, unknown> & {
children?: Node[];
};

export function getCreateDomElement(environment: AutocompleteEnvironment) {
return function createDomElement<KParam extends keyof HTMLElementTagNameMap>(
tagName: KParam,
{ children = [], ...props }: CreateDomElementProps
): HTMLElementTagNameMap[KParam] {
const element = environment.document.createElement<KParam>(tagName);
setProperties(element, props);
element.append(...children);

return element;
};
}
16 changes: 9 additions & 7 deletions packages/autocomplete-js/src/getDefaultOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseItem } from '@algolia/autocomplete-core';
import { AutocompleteEnvironment, BaseItem } from '@algolia/autocomplete-core';
import {
generateAutocompleteId,
invariant,
Expand Down Expand Up @@ -85,16 +85,18 @@ export function getDefaultOptions<TItem extends BaseItem>(
...core
} = options;

const containerElement = getHTMLElement(container);
/* eslint-disable no-restricted-globals */
const environment: AutocompleteEnvironment = (typeof window !== 'undefined'
? window
: {}) as typeof window;
/* eslint-enable no-restricted-globals */
const containerElement = getHTMLElement(environment, container);

invariant(
containerElement.tagName !== 'INPUT',
'The `container` option does not support `input` elements. You need to change the container to a `div`.'
);

const environment = (typeof window !== 'undefined'
? window
: {}) as typeof window;
const defaultedRenderer = renderer ?? defaultRenderer;
const defaultComponents: AutocompleteComponents = {
Highlight: createHighlightComponent(defaultedRenderer),
Expand All @@ -119,8 +121,8 @@ export function getDefaultOptions<TItem extends BaseItem>(
getPanelProps: getPanelProps ?? (({ props }) => props),
getRootProps: getRootProps ?? (({ props }) => props),
panelContainer: panelContainer
? getHTMLElement(panelContainer)
: document.body,
? getHTMLElement(environment, panelContainer)
: environment.document.body,
panelPlacement: panelPlacement ?? 'input-wrapper-width',
render: render ?? defaultRender,
renderNoResults,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ describe('getHTMLElement', () => {
const element = document.createElement('div');
document.body.appendChild(element);

expect(getHTMLElement(element)).toEqual(element);
expect(getHTMLElement(window, element)).toEqual(element);
});

test('with a string returns the element if exists', () => {
const element = document.createElement('div');
document.body.appendChild(element);

expect(getHTMLElement('div')).toEqual(element);
expect(getHTMLElement(window, 'div')).toEqual(element);
});

test('with a string throws invariant if does not exist', () => {
expect(() => {
getHTMLElement('div');
getHTMLElement(window, 'div');
}).toThrow('The element "div" is not in the document.');
});
});
Loading

0 comments on commit 0a33b44

Please sign in to comment.