Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring in nhsuk-frontend ESM syntax JS files to enable compilation to CJS #209

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion bundle-base.tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
"strict": true,
"strictNullChecks": true,
"resolveJsonModule": true,
"allowJs": true,
"outDir": "./dist",
"paths": {
"@components/*": ["src/components/*"],
"@content-presentation/*": ["src/components/content-presentation/*"],
"@form-elements/*": ["src/components/form-elements/*"],
"@navigation/*": ["src/components/navigation/*"],
"@typography/*": ["src/components/typography/*"],
"@util/*": ["src/util/*"],
"@patterns/*": ["src/patterns/*"]
"@patterns/*": ["src/patterns/*"],
"@resources/*": ["src/resources/*"]
}
},
"include": ["src"],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-preserve-directives": "^0.4.0",
"rollup-plugin-tsconfig-paths": "^1.5.2",
"sass": "^1.53.0",
"storybook": "^8.0.5",
"ts-jest": "^29.1.2",
Expand Down
4 changes: 3 additions & 1 deletion rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import external from 'rollup-plugin-peer-deps-external';
import { dts } from 'rollup-plugin-dts';
import tsPaths from 'rollup-plugin-tsconfig-paths';
import preserveDirectives from 'rollup-plugin-preserve-directives';

import tsBuildConfig from './bundle-base.tsconfig.json' assert { type: 'json' };
Expand All @@ -19,7 +20,7 @@ const onWarnSuppression = {
},
};

const commonPlugins = [external(), resolve(), commonjs()];
const commonPlugins = [external(), tsPaths(), resolve(), commonjs()];

export default [
// cjs export
Expand Down Expand Up @@ -64,6 +65,7 @@ export default [
declaration: true,
declarationDir: 'dist/esm',
emitDeclarationOnly: true,
outDir: 'dist/esm',
},
}),
preserveDirectives(),
Expand Down
4 changes: 2 additions & 2 deletions src/components/content-presentation/tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import classNames from 'classnames';
import React, { FC, HTMLAttributes, useEffect } from 'react';
import HeadingLevel, { HeadingLevelType } from '@util/HeadingLevel';
import TabsJs from 'nhsuk-frontend/packages/components/tabs/tabs';
import TabsJs from '@resources/tabs';

type TabsProps = HTMLAttributes<HTMLDivElement>;

Expand Down Expand Up @@ -55,7 +55,7 @@ interface Tabs extends FC<TabsProps> {

const Tabs: Tabs = ({ className, children, ...rest }) => {
useEffect(() => {
TabsJs.default ? TabsJs.default() : TabsJs();
TabsJs();
}, []);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';
import React, { FC, useEffect } from 'react';
import CharacterCountJs from 'nhsuk-frontend/packages/components/character-count/character-count';
import CharacterCountJs from '@resources/character-count';
import { HTMLAttributesWithData } from '@util/types/NHSUKTypes';

export enum CharacterCountType {
Expand All @@ -25,7 +25,7 @@ const CharacterCount: FC<CharacterCountProps> = ({
...rest
}) => {
useEffect(() => {
CharacterCountJs.default ? CharacterCountJs.default() : CharacterCountJs();
CharacterCountJs();
}, []);

const characterCountProps: HTMLAttributesWithData<HTMLDivElement> =
Expand Down
4 changes: 2 additions & 2 deletions src/components/form-elements/checkboxes/Checkboxes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import CheckboxContext, { ICheckboxContext } from './CheckboxContext';
import Box from './components/Box';
import Divider from './components/Divider';
import { generateRandomName } from '@util/RandomID';
import CheckboxJs from 'nhsuk-frontend/packages/components/checkboxes/checkboxes';
import CheckboxJs from '@resources/checkboxes';

interface CheckboxesProps extends HTMLProps<HTMLDivElement>, FormElementProps {
idPrefix?: string;
Expand All @@ -20,7 +20,7 @@ const Checkboxes = ({ children, idPrefix, ...rest }: CheckboxesProps) => {
let _boxIds: Record<string, string> = {};

useEffect(() => {
CheckboxJs.default ? CheckboxJs.default() : CheckboxJs();
CheckboxJs();
}, []);

const getBoxId = (id: string, reference: string): string => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/navigation/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import NavDropdownMenu from './components/NavDropdownMenu';
import { Container } from '@components/layout';
import Content from './components/Content';
import TransactionalServiceName from './components/TransactionalServiceName';
import HeaderJs from 'nhsuk-frontend/packages/components/header/header';
import HeaderJs from '@resources/header';

const BaseHeaderLogo: FC<OrganisationalLogoProps & NHSLogoNavProps> = (props) => {
const { orgName } = useContext<IHeaderContext>(HeaderContext);
Expand Down Expand Up @@ -51,7 +51,7 @@ const Header = ({
const [menuOpen, setMenuOpen] = useState(false);

useEffect(() => {
HeaderJs.default ? HeaderJs.default() : HeaderJs();
HeaderJs();
}, []);

const setMenuToggle = (toggle: boolean): void => {
Expand Down
5 changes: 0 additions & 5 deletions src/global.d.ts

This file was deleted.

262 changes: 262 additions & 0 deletions src/resources/character-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/*
* Lifted from nhsuk-frontend and brought into this repo to enable compilation to CJS if required
* See Github issue https://github.com/nhsuk/nhsuk-frontend/issues/937
*/

class CharacterCount {
constructor($module) {
this.$module = $module;
this.$textarea = $module.querySelector('.nhsuk-js-character-count');
this.$visibleCountMessage = null;
this.$screenReaderCountMessage = null;
this.lastInputTimestamp = null;
}

// Initialize component
init() {
// Check that required elements are present
if (!this.$textarea) {
return;
}

// Check for module
const { $module } = this;
const { $textarea } = this;
const $fallbackLimitMessage = document.getElementById(`${$textarea.id}-info`);

// Move the fallback count message to be immediately after the textarea
// Kept for backwards compatibility
$textarea.insertAdjacentElement('afterend', $fallbackLimitMessage);

// Create the *screen reader* specific live-updating counter
// This doesn't need any styling classes, as it is never visible
const $screenReaderCountMessage = document.createElement('div');
$screenReaderCountMessage.className =
'nhsuk-character-count__sr-status nhsuk-u-visually-hidden';
$screenReaderCountMessage.setAttribute('aria-live', 'polite');
this.$screenReaderCountMessage = $screenReaderCountMessage;
$fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage);

// Create our live-updating counter element, copying the classes from the
// fallback element for backwards compatibility as these may have been configured
const $visibleCountMessage = document.createElement('div');
$visibleCountMessage.className = $fallbackLimitMessage.className;
$visibleCountMessage.classList.add('nhsuk-character-count__status');
$visibleCountMessage.setAttribute('aria-hidden', 'true');
this.$visibleCountMessage = $visibleCountMessage;
$fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage);

// Hide the fallback limit message
$fallbackLimitMessage.classList.add('nhsuk-u-visually-hidden');

// Read options set using dataset ('data-' values)
this.options = CharacterCount.getDataset($module);

// Determine the limit attribute (characters or words)
let countAttribute = this.defaults.characterCountAttribute;
if (this.options.maxwords) {
countAttribute = this.defaults.wordCountAttribute;
}

// Save the element limit
this.maxLength = $module.getAttribute(countAttribute);

// Check for limit
if (!this.maxLength) {
return;
}

// Remove hard limit if set
$textarea.removeAttribute('maxlength');

this.bindChangeEvents();

// When the page is restored after navigating 'back' in some browsers the
// state of the character count is not restored until *after* the DOMContentLoaded
// event is fired, so we need to manually update it after the pageshow event
// in browsers that support it.
if ('onpageshow' in window) {
window.addEventListener('pageshow', this.updateCountMessage.bind(this));
} else {
window.addEventListener('DOMContentLoaded', this.updateCountMessage.bind(this));
}
this.updateCountMessage();
}

// Read data attributes
static getDataset(element) {
const dataset = {};
const { attributes } = element;
if (attributes) {
for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i];
const match = attribute.name.match(/^data-(.+)/);
if (match) {
dataset[match[1]] = attribute.value;
}
}
}
return dataset;
}

// Counts characters or words in text
count(text) {
let length;
if (this.options.maxwords) {
const tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars
length = tokens.length; // eslint-disable-line prefer-destructuring
} else {
length = text.length; // eslint-disable-line prefer-destructuring
}
return length;
}

// Bind input propertychange to the elements and update based on the change
bindChangeEvents() {
const { $textarea } = this;
$textarea.addEventListener('keyup', this.handleKeyUp.bind(this));

// Bind focus/blur events to start/stop polling
$textarea.addEventListener('focus', this.handleFocus.bind(this));
$textarea.addEventListener('blur', this.handleBlur.bind(this));
}

// Speech recognition software such as Dragon NaturallySpeaking will modify the
// fields by directly changing its `value`. These changes don't trigger events
// in JavaScript, so we need to poll to handle when and if they occur.
checkIfValueChanged() {
if (!this.$textarea.oldValue) {
this.$textarea.oldValue = '';
}
if (this.$textarea.value !== this.$textarea.oldValue) {
this.$textarea.oldValue = this.$textarea.value;
this.updateCountMessage();
}
}

// Helper function to update both the visible and screen reader-specific
// counters simultaneously (e.g. on init)
updateCountMessage() {
this.updateVisibleCountMessage();
this.updateScreenReaderCountMessage();
}

// Update visible counter
updateVisibleCountMessage() {
const { $textarea } = this;
const { $visibleCountMessage } = this;
const remainingNumber = this.maxLength - this.count($textarea.value);

// If input is over the threshold, remove the disabled class which renders the
// counter invisible.
if (this.isOverThreshold()) {
$visibleCountMessage.classList.remove('nhsuk-character-count__message--disabled');
} else {
$visibleCountMessage.classList.add('nhsuk-character-count__message--disabled');
}

// Update styles
if (remainingNumber < 0) {
$textarea.classList.add('nhsuk-textarea--error');
$visibleCountMessage.classList.remove('nhsuk-hint');
$visibleCountMessage.classList.add('nhsuk-error-message');
} else {
$textarea.classList.remove('nhsuk-textarea--error');
$visibleCountMessage.classList.remove('nhsuk-error-message');
$visibleCountMessage.classList.add('nhsuk-hint');
}

// Update message
$visibleCountMessage.innerHTML = this.formattedUpdateMessage();
}

// Update screen reader-specific counter
updateScreenReaderCountMessage() {
const { $screenReaderCountMessage } = this;

// If over the threshold, remove the aria-hidden attribute, allowing screen
// readers to announce the content of the element.
if (this.isOverThreshold()) {
$screenReaderCountMessage.removeAttribute('aria-hidden');
} else {
$screenReaderCountMessage.setAttribute('aria-hidden', true);
}

// Update message
$screenReaderCountMessage.innerHTML = this.formattedUpdateMessage();
}

// Format update message
formattedUpdateMessage() {
const { $textarea } = this;
const { options } = this;
const remainingNumber = this.maxLength - this.count($textarea.value);

let charVerb = 'remaining';
let charNoun = 'character';
let displayNumber = remainingNumber;
if (options.maxwords) {
charNoun = 'word';
}
charNoun += remainingNumber === -1 || remainingNumber === 1 ? '' : 's';

charVerb = remainingNumber < 0 ? 'too many' : 'remaining';
displayNumber = Math.abs(remainingNumber);

return `You have ${displayNumber} ${charNoun} ${charVerb}`;
}

// Checks whether the value is over the configured threshold for the input.
// If there is no configured threshold, it is set to 0 and this function will
// always return true.
isOverThreshold() {
const { $textarea } = this;
const { options } = this;

// Determine the remaining number of characters/words
const currentLength = this.count($textarea.value);
const { maxLength } = this;

// Set threshold if presented in options
const thresholdPercent = options.threshold ? options.threshold : 0;
const thresholdValue = (maxLength * thresholdPercent) / 100;

return thresholdValue <= currentLength;
}

// Update the visible character counter and keep track of when the last update
// happened for each keypress
handleKeyUp() {
this.updateVisibleCountMessage();
this.lastInputTimestamp = Date.now();
}

handleFocus() {
// If the field is focused, and a keyup event hasn't been detected for at
// least 1000 ms (1 second), then run the manual change check.
// This is so that the update triggered by the manual comparison doesn't
// conflict with debounced KeyboardEvent updates.
this.valueChecker = setInterval(() => {
if (!this.lastInputTimestamp || Date.now() - 500 >= this.lastInputTimestamp) {
this.checkIfValueChanged();
}
}, 1000);
}

handleBlur() {
// Cancel value checking on blur
clearInterval(this.valueChecker);
}
}

CharacterCount.prototype.defaults = {
characterCountAttribute: 'data-maxlength',
wordCountAttribute: 'data-maxwords',
};

export default ({ scope = document } = {}) => {
const characterCounts = scope.querySelectorAll('[data-module="nhsuk-character-count"]');
characterCounts.forEach((el) => {
new CharacterCount(el).init();
});
};
Loading
Loading