From eefe66e918a01a549a12ed05a2f8f66fa5d4f86f Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 29 Mar 2021 13:08:53 +1100 Subject: [PATCH 01/11] added modals --- src/widgets/modals.ts | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/widgets/modals.ts diff --git a/src/widgets/modals.ts b/src/widgets/modals.ts new file mode 100644 index 000000000..a4722437b --- /dev/null +++ b/src/widgets/modals.ts @@ -0,0 +1,51 @@ +export type ListItem = { + label: string, + link: string +} +export type WindowOptions = { + top: string, + left: string, + coverBackground: boolean +} +// Two functions that need to be implemented to use the modal +// When the user clicks the button, open the modal +/* Click handler on the button to display it. +btn.onclick = function() { + modal.style.display = "block"; +} + +// When the user clicks anywhere outside of the modal, close it +Window click handler so that the modal will close +even if the user doesn't click close +window.onclick = function(event) { + if (event.target == modal) { + modal.style.display = "none"; + } */ +const closeClickHandler = () => { + const modal: HTMLDivElement | null = document.querySelector('#modal') + if (modal) { + modal.style.display = 'none' + } +} +export const createWindow = (list: ListItem[], options: WindowOptions) => { + const modal = document.createElement('div') + modal.id = 'modal' + const modalContent: HTMLDivElement = document.createElement('div') + modalContent.classList.add('modal-content') + const closeButton: HTMLSpanElement = document.createElement('span') + closeButton.classList.add('close') + closeButton.addEventListener('click', closeClickHandler) + const ul: HTMLUListElement = document.createElement('ul') + list.map(list => { + const li:HTMLLIElement = document.createElement('li') + const link: HTMLAnchorElement = document.createElement('a') + link.href = list.link + link.innerHTML = list.label + li.appendChild(link) + ul.appendChild(li) + return li + }) + modalContent.appendChild(closeButton) + modalContent.appendChild(ul) + modal.appendChild(modalContent) +} From 5e2a431c639020f2c003730b57f40c4957651ce3 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 29 Mar 2021 17:19:15 +1100 Subject: [PATCH 02/11] added styles for modal --- src/widgets/modals.ts | 86 ++++++++++++++++++++++++++++---------- src/widgets/modalsStyle.ts | 72 +++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 src/widgets/modalsStyle.ts diff --git a/src/widgets/modals.ts b/src/widgets/modals.ts index a4722437b..515db3e9b 100644 --- a/src/widgets/modals.ts +++ b/src/widgets/modals.ts @@ -1,12 +1,17 @@ +import { + ModalWidgetStyleOptions, + modalStyles, + getModalStyle, + getModalContentStyle, + getModalCloseStyle +} from './modalsStyle' +import { getClasses } from '../jss' + export type ListItem = { label: string, link: string } -export type WindowOptions = { - top: string, - left: string, - coverBackground: boolean -} + // Two functions that need to be implemented to use the modal // When the user clicks the button, open the modal /* Click handler on the button to display it. @@ -22,29 +27,68 @@ window.onclick = function(event) { modal.style.display = "none"; } */ const closeClickHandler = () => { - const modal: HTMLDivElement | null = document.querySelector('#modal') + const modal: HTMLDivElement | null = document.querySelector('.modal') if (modal) { modal.style.display = 'none' } } -export const createWindow = (list: ListItem[], options: WindowOptions) => { - const modal = document.createElement('div') - modal.id = 'modal' - const modalContent: HTMLDivElement = document.createElement('div') - modalContent.classList.add('modal-content') - const closeButton: HTMLSpanElement = document.createElement('span') - closeButton.classList.add('close') + +const createModal = (dom: HTMLDocument, options: ModalWidgetStyleOptions) => { + const modal = dom.createElement('div') + const style = getModalStyle(options) + const { classes } = getClasses(dom.head, { + modal: style + }) + modal.classList.add(classes.modal) + return modal +} + +const createModalContent = (dom: HTMLDocument) => { + const modalContent: HTMLDivElement = dom.createElement('div') + const style = getModalContentStyle() + const { classes } = getClasses(dom.head, { + modalContent: style + }) + modalContent.classList.add(classes.modalContent) + return modalContent +} + +const createCloseButton = (dom: HTMLDocument) => { + const closeButton: HTMLSpanElement = dom.createElement('span') closeButton.addEventListener('click', closeClickHandler) - const ul: HTMLUListElement = document.createElement('ul') - list.map(list => { - const li:HTMLLIElement = document.createElement('li') - const link: HTMLAnchorElement = document.createElement('a') - link.href = list.link - link.innerHTML = list.label - li.appendChild(link) + const style = getModalCloseStyle() + const { classes } = getClasses(dom.head, { + close: style + }) + closeButton.classList.add(classes.close) + return closeButton +} + +const createListItems = (dom: HTMLDocument, list: ListItem) => { + const li:HTMLLIElement = dom.createElement('li') + li.setAttribute('style', modalStyles.listItemStyle) + const link: HTMLAnchorElement = dom.createElement('a') + link.setAttribute('style', modalStyles.anchorStyle) + link.href = list.link + link.innerHTML = list.label + li.appendChild(link) + return li +} + +const createUnOrderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { + const ul: HTMLUListElement = dom.createElement('ul') + ul.setAttribute('style', modalStyles.unorderedListStyle) + listOfLinks.forEach(list => { + const li = createListItems(dom, list) ul.appendChild(li) - return li }) + return ul +} +export const createWindow = (dom: HTMLDocument, listOfLinks: ListItem[], options: ModalWidgetStyleOptions) => { + const modal = createModal(dom, options) + const modalContent = createModalContent(dom) + const closeButton = createCloseButton(dom) + const ul = createUnOrderedList(dom, listOfLinks) modalContent.appendChild(closeButton) modalContent.appendChild(ul) modal.appendChild(modalContent) diff --git a/src/widgets/modalsStyle.ts b/src/widgets/modalsStyle.ts new file mode 100644 index 000000000..3766c1714 --- /dev/null +++ b/src/widgets/modalsStyle.ts @@ -0,0 +1,72 @@ +/** + * Get the button style, based on options. + * See https://design.inrupt.com/atomic-core/?cat=Atoms#Buttons + */ +export type ModalWidgetStyleOptions = { + topPosition?: string, + leftPosition?: string, + withGreyedBackground?: boolean +} + +export const modalStyles = { + unorderedListStyle: 'padding: 0 .2em;', + listItemStyle: 'list-style: none; box-shadow: 1px 1px 1px 1px #888888; padding: .5em', + anchorStyle: 'text-decoration: none' +} + +export const getModalStyle = (options: ModalWidgetStyleOptions = {}) => { + const topPosition = (options.topPosition) ? options.topPosition : '50px' + const leftPosition = (options.leftPosition) ? options.leftPosition : '50px' + + if (options.withGreyedBackground) { + return { + display: 'none', + position: 'fixed', + 'z-index': '1', + overflow: 'auto', /* Enable scroll if needed */ + 'background-color': 'rgba(0,0,0,0.4)', /* Black w/ opacity */ + 'padding-top': '100px', + left: '0', + top: '0', + width: '100%', + height: '100%' + } + } + + return { + display: 'none', + position: 'fixed', + 'z-index': '1', + top: `${topPosition}`, + left: `${leftPosition}`, + overflow: 'auto', /* Enable scroll if needed */ + 'background-color': 'rgba(0,0,0,0.4)' /* Black w/ opacity */ + } +} + +export const getModalContentStyle = () => { + return { + display: 'flex', + 'flex-direction': 'column', + 'background-color': '#fefefe' + } +} + +export const getModalCloseStyle = () => { + return { + color: '#aaaaaa', + 'align-self': 'flex-end', + 'font-size': '20px', + 'font-weight': 'bold', + 'margin-right': '.2em', + 'margin-top': '.2em', + '&:hover': { + 'text-decoration': 'none', + cursor: 'pointer' + }, + '&:focus': { + 'text-decoration': 'none', + cursor: 'pointer' + } + } +} From f947e1a6b797e25ec8a889084af9da04184ba26c Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 29 Mar 2021 20:49:46 +1100 Subject: [PATCH 03/11] exported modals --- src/widgets/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widgets/index.js b/src/widgets/index.js index aed84309c..4ccd6fc88 100644 --- a/src/widgets/index.js +++ b/src/widgets/index.js @@ -26,7 +26,8 @@ const widgets = Object.assign( require('./dragAndDrop'), // uploadFiles etc require('./error'), // UI.widgets.errorMessageBlock require('./buttons'), - require('./forms') + require('./forms'), + require('./modals') ) module.exports = widgets From e05ffe1e2e54ad4ab335c1f3359e4bcdc6a533fb Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 07:14:05 +1100 Subject: [PATCH 04/11] changed style to new way --- src/style.js | 9 ++++++ src/widgets/modals.ts | 43 ++++++++++++++--------------- src/widgets/modalsStyle.ts | 56 ++------------------------------------ 3 files changed, 32 insertions(+), 76 deletions(-) diff --git a/src/style.js b/src/style.js index 187160c85..2b0ec37de 100644 --- a/src/style.js +++ b/src/style.js @@ -63,6 +63,15 @@ export const style = { // styleModule imageDivStyle: 'width:2.5em; padding:0.5em; height: 2.5em;', linkDivStyle: 'width:2em; padding:0.5em; height: 4em;', + // modals + modalUnorderedListStyle: 'padding: 0 .2em;', + modalListItemStyle: 'list-style: none; box-shadow: 1px 1px 1px 1px #888888; padding: .5em;', + modalAnchorStyle: 'text-decoration: none;', + modalContentStyle: 'display: flex; flex-direction: column; background-color: #fefefe;', + modalCloseStyle: 'color: #aaaaaa; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; cursor: pointer;', + modalCloseStyleHover: 'color: #666666; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; text-decoration: none; cursor: pointer;', + modalCloseStyleFocus: 'color: #666666; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; text-decoration: none; cursor: pointer; outline: 1px solid #888;', + // ACL aclControlBoxContainer: 'margin: 1em;', aclControlBoxHeader: 'font-size: 120%; margin: 0 0 1rem;', diff --git a/src/widgets/modals.ts b/src/widgets/modals.ts index 515db3e9b..fce400cd8 100644 --- a/src/widgets/modals.ts +++ b/src/widgets/modals.ts @@ -1,11 +1,8 @@ import { ModalWidgetStyleOptions, - modalStyles, - getModalStyle, - getModalContentStyle, - getModalCloseStyle + getModalStyle } from './modalsStyle' -import { getClasses } from '../jss' +import { style } from '../style' export type ListItem = { label: string, @@ -35,40 +32,42 @@ const closeClickHandler = () => { const createModal = (dom: HTMLDocument, options: ModalWidgetStyleOptions) => { const modal = dom.createElement('div') - const style = getModalStyle(options) - const { classes } = getClasses(dom.head, { - modal: style - }) - modal.classList.add(classes.modal) + modal.classList.add('modal') + modal.setAttribute('style', getModalStyle(options)) return modal } const createModalContent = (dom: HTMLDocument) => { const modalContent: HTMLDivElement = dom.createElement('div') - const style = getModalContentStyle() - const { classes } = getClasses(dom.head, { - modalContent: style - }) - modalContent.classList.add(classes.modalContent) + modalContent.setAttribute('style', style.modalContentStyle) return modalContent } const createCloseButton = (dom: HTMLDocument) => { const closeButton: HTMLSpanElement = dom.createElement('span') closeButton.addEventListener('click', closeClickHandler) - const style = getModalCloseStyle() - const { classes } = getClasses(dom.head, { - close: style + closeButton.addEventListener('mouseenter', () => { + closeButton.setAttribute('style', style.modalCloseStyleHover) + }) + closeButton.addEventListener('mouseleave', () => { + closeButton.setAttribute('style', style.modalCloseStyle) + }) + closeButton.addEventListener('focus', () => { + closeButton.setAttribute('style', style.modalCloseStyleFocus) + }) + closeButton.addEventListener('blur', () => { + closeButton.setAttribute('style', style.modalCloseStyle) }) - closeButton.classList.add(classes.close) + closeButton.setAttribute('tabindex', '0') + closeButton.setAttribute('style', style.modalCloseStyle) return closeButton } const createListItems = (dom: HTMLDocument, list: ListItem) => { const li:HTMLLIElement = dom.createElement('li') - li.setAttribute('style', modalStyles.listItemStyle) + li.setAttribute('style', style.modalListItemStyle) const link: HTMLAnchorElement = dom.createElement('a') - link.setAttribute('style', modalStyles.anchorStyle) + link.setAttribute('style', style.modalAnchorStyle) link.href = list.link link.innerHTML = list.label li.appendChild(link) @@ -77,7 +76,7 @@ const createListItems = (dom: HTMLDocument, list: ListItem) => { const createUnOrderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { const ul: HTMLUListElement = dom.createElement('ul') - ul.setAttribute('style', modalStyles.unorderedListStyle) + ul.setAttribute('style', style.modalUnorderedListStyle) listOfLinks.forEach(list => { const li = createListItems(dom, list) ul.appendChild(li) diff --git a/src/widgets/modalsStyle.ts b/src/widgets/modalsStyle.ts index 3766c1714..a69fc3e76 100644 --- a/src/widgets/modalsStyle.ts +++ b/src/widgets/modalsStyle.ts @@ -8,65 +8,13 @@ export type ModalWidgetStyleOptions = { withGreyedBackground?: boolean } -export const modalStyles = { - unorderedListStyle: 'padding: 0 .2em;', - listItemStyle: 'list-style: none; box-shadow: 1px 1px 1px 1px #888888; padding: .5em', - anchorStyle: 'text-decoration: none' -} - export const getModalStyle = (options: ModalWidgetStyleOptions = {}) => { const topPosition = (options.topPosition) ? options.topPosition : '50px' const leftPosition = (options.leftPosition) ? options.leftPosition : '50px' if (options.withGreyedBackground) { - return { - display: 'none', - position: 'fixed', - 'z-index': '1', - overflow: 'auto', /* Enable scroll if needed */ - 'background-color': 'rgba(0,0,0,0.4)', /* Black w/ opacity */ - 'padding-top': '100px', - left: '0', - top: '0', - width: '100%', - height: '100%' - } + return 'display: none; position: fixed; z-index: 1; overflow: auto; background-color: rgba(0,0,0,0.4); padding-top: 100px; left: 0; top: 0; width: 100%; height: 100%;' } - return { - display: 'none', - position: 'fixed', - 'z-index': '1', - top: `${topPosition}`, - left: `${leftPosition}`, - overflow: 'auto', /* Enable scroll if needed */ - 'background-color': 'rgba(0,0,0,0.4)' /* Black w/ opacity */ - } -} - -export const getModalContentStyle = () => { - return { - display: 'flex', - 'flex-direction': 'column', - 'background-color': '#fefefe' - } -} - -export const getModalCloseStyle = () => { - return { - color: '#aaaaaa', - 'align-self': 'flex-end', - 'font-size': '20px', - 'font-weight': 'bold', - 'margin-right': '.2em', - 'margin-top': '.2em', - '&:hover': { - 'text-decoration': 'none', - cursor: 'pointer' - }, - '&:focus': { - 'text-decoration': 'none', - cursor: 'pointer' - } - } + return `display: none; position: fixed; z-index: 1; top: ${topPosition}; left: ${leftPosition}; overflow: auto; background-color: rgba(0,0,0,0.4);` } From e5b41100efe1b5353d9e07f0488659fe83308a68 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 07:52:51 +1100 Subject: [PATCH 05/11] avoid doc-wide .modal lookup --- src/widgets/modals.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/widgets/modals.ts b/src/widgets/modals.ts index fce400cd8..669e90614 100644 --- a/src/widgets/modals.ts +++ b/src/widgets/modals.ts @@ -23,11 +23,8 @@ window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; } */ -const closeClickHandler = () => { - const modal: HTMLDivElement | null = document.querySelector('.modal') - if (modal) { - modal.style.display = 'none' - } +const closeClickHandler = (modal: HTMLDivElement) => { + modal.style.display = 'none' } const createModal = (dom: HTMLDocument, options: ModalWidgetStyleOptions) => { @@ -43,9 +40,9 @@ const createModalContent = (dom: HTMLDocument) => { return modalContent } -const createCloseButton = (dom: HTMLDocument) => { +const createCloseButton = (dom: HTMLDocument, modal: HTMLDivElement) => { const closeButton: HTMLSpanElement = dom.createElement('span') - closeButton.addEventListener('click', closeClickHandler) + closeButton.addEventListener('click', () => closeClickHandler(modal)) closeButton.addEventListener('mouseenter', () => { closeButton.setAttribute('style', style.modalCloseStyleHover) }) @@ -86,7 +83,7 @@ const createUnOrderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { export const createWindow = (dom: HTMLDocument, listOfLinks: ListItem[], options: ModalWidgetStyleOptions) => { const modal = createModal(dom, options) const modalContent = createModalContent(dom) - const closeButton = createCloseButton(dom) + const closeButton = createCloseButton(dom, modal) const ul = createUnOrderedList(dom, listOfLinks) modalContent.appendChild(closeButton) modalContent.appendChild(ul) From 1b682fca95441f65df65b65794f3cb5b9f669a50 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 10:21:45 +1100 Subject: [PATCH 06/11] improve name and return modal --- src/style.js | 6 +++--- src/widgets/modals.ts | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/style.js b/src/style.js index 2b0ec37de..e3adcf6fa 100644 --- a/src/style.js +++ b/src/style.js @@ -68,9 +68,9 @@ export const style = { // styleModule modalListItemStyle: 'list-style: none; box-shadow: 1px 1px 1px 1px #888888; padding: .5em;', modalAnchorStyle: 'text-decoration: none;', modalContentStyle: 'display: flex; flex-direction: column; background-color: #fefefe;', - modalCloseStyle: 'color: #aaaaaa; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; cursor: pointer;', - modalCloseStyleHover: 'color: #666666; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; text-decoration: none; cursor: pointer;', - modalCloseStyleFocus: 'color: #666666; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; text-decoration: none; cursor: pointer; outline: 1px solid #888;', + modalCloseStyle: 'background: none; border: 0; padding: 0; color: #aaaaaa; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; cursor: pointer;', + modalCloseStyleHover: 'background: none; border: 0; padding: 0; color: #666666; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; text-decoration: none; cursor: pointer;', + modalCloseStyleFocus: 'background: none; border: 0; padding: 0; color: #666666; align-self: flex-end; font-size: 20px; font-weight: bold; margin-right: .2em; margin-top: .2em; text-decoration: none; cursor: pointer; outline: 1px solid #888;', // ACL aclControlBoxContainer: 'margin: 1em;', diff --git a/src/widgets/modals.ts b/src/widgets/modals.ts index 669e90614..8bf966956 100644 --- a/src/widgets/modals.ts +++ b/src/widgets/modals.ts @@ -41,7 +41,10 @@ const createModalContent = (dom: HTMLDocument) => { } const createCloseButton = (dom: HTMLDocument, modal: HTMLDivElement) => { - const closeButton: HTMLSpanElement = dom.createElement('span') + const closeButton: HTMLButtonElement = dom.createElement('button') + closeButton.setAttribute('type', 'button') + closeButton.setAttribute('aria-label', 'Close modal') + closeButton.textContent = 'x' closeButton.addEventListener('click', () => closeClickHandler(modal)) closeButton.addEventListener('mouseenter', () => { closeButton.setAttribute('style', style.modalCloseStyleHover) @@ -55,7 +58,6 @@ const createCloseButton = (dom: HTMLDocument, modal: HTMLDivElement) => { closeButton.addEventListener('blur', () => { closeButton.setAttribute('style', style.modalCloseStyle) }) - closeButton.setAttribute('tabindex', '0') closeButton.setAttribute('style', style.modalCloseStyle) return closeButton } @@ -71,7 +73,7 @@ const createListItems = (dom: HTMLDocument, list: ListItem) => { return li } -const createUnOrderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { +const createUnorderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { const ul: HTMLUListElement = dom.createElement('ul') ul.setAttribute('style', style.modalUnorderedListStyle) listOfLinks.forEach(list => { @@ -80,12 +82,14 @@ const createUnOrderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { }) return ul } -export const createWindow = (dom: HTMLDocument, listOfLinks: ListItem[], options: ModalWidgetStyleOptions) => { +export const createListModal = (dom: HTMLDocument, listOfLinks: ListItem[], options: ModalWidgetStyleOptions) => { const modal = createModal(dom, options) const modalContent = createModalContent(dom) const closeButton = createCloseButton(dom, modal) - const ul = createUnOrderedList(dom, listOfLinks) + const ul = createUnorderedList(dom, listOfLinks) modalContent.appendChild(closeButton) modalContent.appendChild(ul) modal.appendChild(modalContent) + + return modal } From 6a798d803888ffcf300e2d92c3c5a4817036cfb6 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 10:22:22 +1100 Subject: [PATCH 07/11] Apply suggestions from code review Co-authored-by: Ted Thibodeau Jr Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/widgets/modals.ts | 17 +++++++++-------- src/widgets/modalsStyle.ts | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/widgets/modals.ts b/src/widgets/modals.ts index 515db3e9b..1531df488 100644 --- a/src/widgets/modals.ts +++ b/src/widgets/modals.ts @@ -12,16 +12,16 @@ export type ListItem = { link: string } -// Two functions that need to be implemented to use the modal -// When the user clicks the button, open the modal +// Two functions that need to be implemented to use the modal. +// When the user clicks the button, open the modal. /* Click handler on the button to display it. btn.onclick = function() { modal.style.display = "block"; } -// When the user clicks anywhere outside of the modal, close it -Window click handler so that the modal will close -even if the user doesn't click close +// When the user clicks anywhere outside of the modal, close it. +// Window click handler so that the modal will close +// even if the user doesn't click close. window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; @@ -70,7 +70,7 @@ const createListItems = (dom: HTMLDocument, list: ListItem) => { const link: HTMLAnchorElement = dom.createElement('a') link.setAttribute('style', modalStyles.anchorStyle) link.href = list.link - link.innerHTML = list.label + link.textContent = list.label li.appendChild(link) return li } @@ -84,8 +84,9 @@ const createUnOrderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { }) return ul } -export const createWindow = (dom: HTMLDocument, listOfLinks: ListItem[], options: ModalWidgetStyleOptions) => { - const modal = createModal(dom, options) +export const createWindow = (dom: HTMLDocument, listOfLinks: ListItem[], options?: ModalWidgetStyleOptions) => { + const modalOptions = (options ?? {}) as ModalWidgetStyleOptions + const modal = createModal(dom, modalOptions) const modalContent = createModalContent(dom) const closeButton = createCloseButton(dom) const ul = createUnOrderedList(dom, listOfLinks) diff --git a/src/widgets/modalsStyle.ts b/src/widgets/modalsStyle.ts index 3766c1714..bee1d7b2a 100644 --- a/src/widgets/modalsStyle.ts +++ b/src/widgets/modalsStyle.ts @@ -1,6 +1,6 @@ /** - * Get the button style, based on options. - * See https://design.inrupt.com/atomic-core/?cat=Atoms#Buttons + * Get the modal styles, based on options such as position and background overlay. + * See https://design.inrupt.com/atomic-core/?cat=Organisms#Modals for modal design guidelines. */ export type ModalWidgetStyleOptions = { topPosition?: string, From da4fe114774f72d04c111e69bd506f058aa0ebf6 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 10:31:51 +1100 Subject: [PATCH 08/11] lint fix --- src/acl/access-groups.ts | 2 +- src/pad.ts | 44 ++++++++++++++++++++-------------------- test/helpers/setup.ts | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/acl/access-groups.ts b/src/acl/access-groups.ts index 63f4dd7af..695473750 100644 --- a/src/acl/access-groups.ts +++ b/src/acl/access-groups.ts @@ -264,7 +264,7 @@ export class AccessGroups { : 'No RDF type was detected for this URI.' const error = `Error: Failed to add access target: ${uri} is not a recognized ACL target type.` + - ` Expected one of: vcard:WebID, vcard:Group, foaf:Person, foaf:Agent, solid:AppProvider, solid:AppProviderClass, or recognized ACL classes.` + + ' Expected one of: vcard:WebID, vcard:Group, foaf:Person, foaf:Agent, solid:AppProvider, solid:AppProviderClass, or recognized ACL classes.' + ' Hint: try dropping a WebID profile URI, a vcard:Group URI, or a web app origin.' + typeDetails debug.error(error) diff --git a/src/pad.ts b/src/pad.ts index 8ffcd9a6e..fca8f8736 100644 --- a/src/pad.ts +++ b/src/pad.ts @@ -46,7 +46,7 @@ class NotepadPart extends HTMLElement { * @param {NamedNode} author - The author of text being displayed * @returns {String} The CSS color generated, constrained to be light for a background color */ -export function lightColorHash(author?: NamedNode): string { +export function lightColorHash (author?: NamedNode): string { const hash = function (x) { return x.split('').reduce(function (a, b) { a = (a << 5) - a + b.charCodeAt(0) @@ -66,7 +66,7 @@ export function lightColorHash(author?: NamedNode): string { * @param {NamedNode} me - person who is logged into the pod * @param {notepadOptions} options - the options that can be passed in consist of statusArea, exists */ -export function notepad( +export function notepad ( dom: HTMLDocument, padDoc: NamedNode, subject: NamedNode, @@ -164,7 +164,7 @@ export function notepad( const next: any = kb.any(chunk as any, PAD('next')) if (prev.sameTerm(subject) && next.sameTerm(subject)) { // Last one - log("You can't delete the only line.") + log('You can\'t delete the only line.') return } @@ -210,7 +210,7 @@ export function notepad( }, 1000) } else { log(' removePart FAILED ' + chunk + ': ' + errorMessage) - log(" removePart was deleting :'" + del) + log(' removePart was deleting :\'' + del) setPartStyle(part, 'color: black; background-color: #fdd;') // failed const res = response ? (response as any).status @@ -235,9 +235,9 @@ export function notepad( updater.update(del, ins as any, function (uri, ok, errorBody) { if (!ok) { log( - "Indent change FAILED '" + + 'Indent change FAILED \'' + newIndent + - "' for " + + '\' for ' + padDoc + ': ' + errorBody @@ -351,11 +351,11 @@ export function notepad( if (old !== part.lastSent) { // Non-fatal: log a warning instead of throwing, to avoid crashing the pad UI. console.warn( - "Out of order, last sent expected '" + + 'Out of order, last sent expected \'' + old + - "' but found '" + + '\' but found \'' + part.lastSent + - "'" + '\'' ) } } @@ -380,11 +380,11 @@ export function notepad( log( ' patch FAILED ' + (xhr as any).status + - " for '" + + ' for \'' + old + - "' -> '" + + '\' -> \'' + newOne + - "': " + + '\': ' + errorBody ) if ((xhr as any).status === 409) { @@ -421,7 +421,7 @@ export function notepad( } else { clearStatus(true) // upstream setPartStyle(part) // synced - log(" Patch ok '" + old + "' -> '" + newOne + "' ") + log(' Patch ok \'' + old + '\' -> \'' + newOne + '\' ') if (part.state === 4) { // delete me @@ -440,10 +440,10 @@ export function notepad( }) } - part.addEventListener('input', function inputChangeListener(_event) { + part.addEventListener('input', function inputChangeListener (_event) { // debug.log("input changed "+part.value); setPartStyle(part, undefined, true) // grey out - not synced - log('Input event state ' + part.state + " value '" + part.value + "'") + log('Input event state ' + part.state + ' value \'' + part.value + '\'') switch (part.state) { case 3: // being deleted return @@ -498,7 +498,7 @@ export function notepad( addListeners(part, chunk) } else { setPartStyle(part, 'color: #222; background-color: #fff') - log("Note can't add listeners - not logged in") + log('Note can\'t add listeners - not logged in') } return part } @@ -584,7 +584,7 @@ export function notepad( const consistencyCheck = function () { const found: { [uri: string]: boolean } = {} let failed = 0 - function complain2(msg) { + function complain2 (msg) { complain(msg) failed++ } @@ -831,7 +831,7 @@ export function notepad( */ // @ignore exporting this only for the unit test -export function getChunks(subject: NamedNode, kb: IndexedFormula) { +export function getChunks (subject: NamedNode, kb: IndexedFormula) { const chunks: any[] = [] for ( let chunk: any = kb.the(subject, PAD('next')); @@ -847,7 +847,7 @@ export function getChunks(subject: NamedNode, kb: IndexedFormula) { * Encode content to be put in XML or HTML elements */ // @ignore exporting this only for the unit test -export function xmlEncode(str) { +export function xmlEncode (str) { return str.replace('&', '&').replace('<', '<').replace('>', '>') } @@ -856,7 +856,7 @@ export function xmlEncode(str) { * @param { } pad - the notepad * @param {store} pad - the data store */ -export function notepadToHTML(pad: any, kb: IndexedFormula) { +export function notepadToHTML (pad: any, kb: IndexedFormula) { const chunks = getChunks(pad, kb) let html = '\n \n' const title = kb.anyValue(pad, ns.dct('title')) @@ -866,13 +866,13 @@ export function notepadToHTML(pad: any, kb: IndexedFormula) { html += ' \n \n' let level = 0 - function increaseLevel(indent) { + function increaseLevel (indent) { for (; level < indent; level++) { html += '
    \n' } } - function decreaseLevel(indent) { + function decreaseLevel (indent) { for (; level > indent; level--) { html += '
\n' } diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts index e87a158b3..3f2e397c4 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -21,7 +21,7 @@ global.WritableStream = WritableStream // Node provides MessagePort via worker_threads; jsdom/undici expects it in global scope try { - // eslint-disable-next-line @typescript-eslint/no-var-requires + // Intentionally require worker_threads in Jest setup to expose MessagePort globals. const { MessageChannel, MessagePort } = require('worker_threads') global.MessageChannel = MessageChannel global.MessagePort = MessagePort From 7eb175b685391c7ef8e9322f5578e64dbd7611df Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 10:37:19 +1100 Subject: [PATCH 09/11] accessibility --- src/widgets/modals.ts | 33 +++++++++++++++++++++++++++------ src/widgets/modalsStyle.ts | 3 ++- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/widgets/modals.ts b/src/widgets/modals.ts index e32fd59a1..c1be37925 100644 --- a/src/widgets/modals.ts +++ b/src/widgets/modals.ts @@ -23,13 +23,25 @@ window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; } */ -const closeClickHandler = (modal: HTMLDivElement) => { +const closeClickHandler = (modal: HTMLDivElement, returnFocusTo?: Element | null) => { modal.style.display = 'none' + if (returnFocusTo && returnFocusTo instanceof HTMLElement) { + returnFocusTo.focus() + } } -const createModal = (dom: HTMLDocument, options: ModalWidgetStyleOptions) => { +const createModal = (dom: HTMLDocument, options: ModalWidgetStyleOptions, returnFocusTo?: Element | null) => { const modal = dom.createElement('div') modal.classList.add('modal') + modal.setAttribute('role', 'dialog') + modal.setAttribute('aria-modal', options.withGreyedBackground ? 'true' : 'false') + modal.setAttribute('aria-label', options.ariaLabel || 'List dialog') + modal.setAttribute('tabindex', '-1') + modal.addEventListener('keydown', event => { + if (event.key === 'Escape') { + closeClickHandler(modal, returnFocusTo) + } + }) modal.setAttribute('style', getModalStyle(options)) return modal } @@ -40,12 +52,13 @@ const createModalContent = (dom: HTMLDocument) => { return modalContent } -const createCloseButton = (dom: HTMLDocument, modal: HTMLDivElement) => { +const createCloseButton = (dom: HTMLDocument, modal: HTMLDivElement, returnFocusTo?: Element | null) => { const closeButton: HTMLButtonElement = dom.createElement('button') closeButton.setAttribute('type', 'button') closeButton.setAttribute('aria-label', 'Close modal') + closeButton.setAttribute('title', 'Close') closeButton.textContent = 'x' - closeButton.addEventListener('click', () => closeClickHandler(modal)) + closeButton.addEventListener('click', () => closeClickHandler(modal, returnFocusTo)) closeButton.addEventListener('mouseenter', () => { closeButton.setAttribute('style', style.modalCloseStyleHover) }) @@ -75,6 +88,7 @@ const createListItems = (dom: HTMLDocument, list: ListItem) => { const createUnorderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { const ul: HTMLUListElement = dom.createElement('ul') + ul.setAttribute('role', 'list') ul.setAttribute('style', style.modalUnorderedListStyle) listOfLinks.forEach(list => { const li = createListItems(dom, list) @@ -83,13 +97,20 @@ const createUnorderedList = (dom: HTMLDocument, listOfLinks: ListItem[]) => { return ul } export const createListModal = (dom: HTMLDocument, listOfLinks: ListItem[], options: ModalWidgetStyleOptions) => { - const modal = createModal(dom, options) + const returnFocusTo = dom.activeElement + const modal = createModal(dom, options, returnFocusTo) const modalContent = createModalContent(dom) - const closeButton = createCloseButton(dom, modal) + const closeButton = createCloseButton(dom, modal, returnFocusTo) const ul = createUnorderedList(dom, listOfLinks) modalContent.appendChild(closeButton) modalContent.appendChild(ul) modal.appendChild(modalContent) + setTimeout(() => { + if (closeButton.isConnected) { + closeButton.focus() + } + }, 0) + return modal } diff --git a/src/widgets/modalsStyle.ts b/src/widgets/modalsStyle.ts index fe48b5a62..f84720184 100644 --- a/src/widgets/modalsStyle.ts +++ b/src/widgets/modalsStyle.ts @@ -5,7 +5,8 @@ export type ModalWidgetStyleOptions = { topPosition?: string, leftPosition?: string, - withGreyedBackground?: boolean + withGreyedBackground?: boolean, + ariaLabel?: string } export const getModalStyle = (options: ModalWidgetStyleOptions = {}) => { From 9426f6cb03ac3a397002009894fd1d1246b99885 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 10:37:26 +1100 Subject: [PATCH 10/11] tests --- test/unit/widgets/modals.test.ts | 82 ++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/unit/widgets/modals.test.ts diff --git a/test/unit/widgets/modals.test.ts b/test/unit/widgets/modals.test.ts new file mode 100644 index 000000000..6205ff7a8 --- /dev/null +++ b/test/unit/widgets/modals.test.ts @@ -0,0 +1,82 @@ +import { silenceDebugMessages } from '../helpers/debugger' +import { createListModal } from '../../../src/widgets/modals' + +silenceDebugMessages() + +describe('modals', () => { + afterEach(() => { + jest.useRealTimers() + document.body.innerHTML = '' + }) + + it('creates a dialog modal with default accessibility attributes', () => { + const modal = createListModal(document, [], {}) + + expect(modal).toBeInstanceOf(HTMLDivElement) + expect(modal.getAttribute('role')).toEqual('dialog') + expect(modal.getAttribute('aria-modal')).toEqual('false') + expect(modal.getAttribute('aria-label')).toEqual('List dialog') + expect(modal.getAttribute('tabindex')).toEqual('-1') + }) + + it('renders list items with links and custom aria label', () => { + const modal = createListModal( + document, + [{ label: 'Solid', link: 'https://solidproject.org/' }], + { ariaLabel: 'Resource links', withGreyedBackground: true } + ) + + const list = modal.querySelector('ul') as HTMLUListElement + const anchor = modal.querySelector('a') as HTMLAnchorElement + + expect(list.getAttribute('role')).toEqual('list') + expect(anchor.textContent).toEqual('Solid') + expect(anchor.href).toEqual('https://solidproject.org/') + expect(modal.getAttribute('aria-label')).toEqual('Resource links') + expect(modal.getAttribute('aria-modal')).toEqual('true') + }) + + it('uses an accessible close button', () => { + const modal = createListModal(document, [], {}) + const closeButton = modal.querySelector('button') as HTMLButtonElement + + expect(closeButton).toBeInstanceOf(HTMLButtonElement) + expect(closeButton.getAttribute('type')).toEqual('button') + expect(closeButton.getAttribute('aria-label')).toEqual('Close modal') + }) + + it('closes on close button click and restores focus to trigger element', () => { + jest.useFakeTimers() + + const trigger = document.createElement('button') + document.body.appendChild(trigger) + trigger.focus() + + const modal = createListModal(document, [], {}) + document.body.appendChild(modal) + + jest.runAllTimers() + + const closeButton = modal.querySelector('button') as HTMLButtonElement + modal.style.display = 'block' + closeButton.click() + + expect(modal.style.display).toEqual('none') + expect(document.activeElement).toBe(trigger) + }) + + it('closes on Escape and restores focus to trigger element', () => { + const trigger = document.createElement('button') + document.body.appendChild(trigger) + trigger.focus() + + const modal = createListModal(document, [], {}) + document.body.appendChild(modal) + modal.style.display = 'block' + + modal.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) + + expect(modal.style.display).toEqual('none') + expect(document.activeElement).toBe(trigger) + }) +}) From a99b5ff14db613a281574eb0152e7c2ed06b28be Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Thu, 26 Mar 2026 10:43:16 +1100 Subject: [PATCH 11/11] added stories --- src/stories/Modals.stories.js | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/stories/Modals.stories.js diff --git a/src/stories/Modals.stories.js b/src/stories/Modals.stories.js new file mode 100644 index 000000000..b632cddf8 --- /dev/null +++ b/src/stories/Modals.stories.js @@ -0,0 +1,55 @@ +import * as UI from '../../src/index' + +export default { + title: 'Modals', +} + +const demoLinks = [ + { label: 'Solid Project', link: 'https://solidproject.org/' }, + { label: 'SolidOS', link: 'https://github.com/solidos' }, + { label: 'W3C', link: 'https://www.w3.org/' }, +] + +export const ListModal = { + render: () => { + const modal = UI.widgets.createListModal(document, demoLinks, { + withGreyedBackground: true, + ariaLabel: 'Helpful links', + }) + modal.style.display = 'block' + return modal + }, + name: 'List modal', +} + +export const OpenListModal = { + render: () => { + const container = document.createElement('div') + + const openButton = document.createElement('button') + openButton.setAttribute('type', 'button') + openButton.textContent = 'Open links modal' + + const helper = document.createElement('p') + helper.textContent = 'Open the modal, then press Escape or use the close button.' + + openButton.addEventListener('click', () => { + const existing = container.querySelector('.modal') + if (existing) { + existing.remove() + } + + const modal = UI.widgets.createListModal(document, demoLinks, { + withGreyedBackground: true, + ariaLabel: 'Helpful links', + }) + modal.style.display = 'block' + container.appendChild(modal) + }) + + container.appendChild(openButton) + container.appendChild(helper) + return container + }, + name: 'Open list modal', +}