From 853f3f5280c66828c61a62f17eb5b38115312d61 Mon Sep 17 00:00:00 2001 From: Nandor_Czegledi Date: Fri, 28 Aug 2026 11:34:03 +0200 Subject: [PATCH 1/2] fix(ui-instructure): add an info icon to AI Information dialog triggers for WCAG 1.4.1 compliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported from Oak as AGN-657 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) --- .../__tests__/DataPermissionLevels.test.tsx | 78 ++++++++++++++++++ .../src/DataPermissionLevels/v2/index.tsx | 6 +- .../src/DataPermissionLevels/v2/props.ts | 1 + .../src/DataPermissionLevels/v2/styles.ts | 19 ++++- .../__tests__/NutritionFacts.test.tsx | 81 +++++++++++++++++++ .../src/NutritionFacts/v2/index.tsx | 6 +- .../src/NutritionFacts/v2/props.ts | 1 + .../src/NutritionFacts/v2/styles.ts | 16 +++- 8 files changed, 200 insertions(+), 8 deletions(-) create mode 100644 packages/ui-instructure/src/DataPermissionLevels/__tests__/DataPermissionLevels.test.tsx create mode 100644 packages/ui-instructure/src/NutritionFacts/__tests__/NutritionFacts.test.tsx diff --git a/packages/ui-instructure/src/DataPermissionLevels/__tests__/DataPermissionLevels.test.tsx b/packages/ui-instructure/src/DataPermissionLevels/__tests__/DataPermissionLevels.test.tsx new file mode 100644 index 0000000000..2d246dad09 --- /dev/null +++ b/packages/ui-instructure/src/DataPermissionLevels/__tests__/DataPermissionLevels.test.tsx @@ -0,0 +1,78 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { render } from 'vitest-browser-react' +import { describe, it, expect } from 'vitest' +import { DataPermissionLevels } from '@instructure/ui-instructure/latest' + +describe('', () => { + const props = { + modalLabel: 'Data permission levels modal', + title: 'Data Permission Levels', + currentFeatureText: 'Current Feature:', + currentFeature: 'Feature name', + closeButtonText: 'Close', + closeIconButtonScreenReaderLabel: 'Close', + triggerText: 'Permission Levels', + data: [ + { + level: 'LEVEL 1', + title: 'Descriptive Analytics and Research', + description: 'We leverage anonymized aggregate data.', + highlighted: true + } + ] + } + + describe('trigger', () => { + it('should render as a button that announces it opens a dialog', async () => { + await render() + + const trigger = document.querySelector('button') + + expect(trigger).toBeInTheDocument() + expect(trigger).toHaveAttribute('aria-haspopup', 'dialog') + expect(trigger).toHaveTextContent(props.triggerText) + }) + + it('should render an icon after the trigger text', async () => { + await render() + + const icon = document.querySelector('button svg') + + expect(icon).toBeInTheDocument() + // the icon is a suffix cue, so the label text must directly precede it + expect(icon!.parentElement!.previousSibling?.textContent).toBe( + props.triggerText + ) + }) + + it('should keep the icon decorative so it does not alter the accessible name', async () => { + await render() + + const icon = document.querySelector('button svg') + + expect(icon).toHaveAttribute('aria-hidden', 'true') + }) + }) +}) diff --git a/packages/ui-instructure/src/DataPermissionLevels/v2/index.tsx b/packages/ui-instructure/src/DataPermissionLevels/v2/index.tsx index b1b625bd3e..6fc29097f9 100644 --- a/packages/ui-instructure/src/DataPermissionLevels/v2/index.tsx +++ b/packages/ui-instructure/src/DataPermissionLevels/v2/index.tsx @@ -30,6 +30,7 @@ import { } from '@instructure/ui-buttons/latest' import { Heading } from '@instructure/ui-heading/latest' import { Text } from '@instructure/ui-text/latest' +import { InfoInstUIIcon } from '@instructure/ui-icons' import { useStyleNew } from '@instructure/emotion' import { DataPermissionLevelsProps } from './props.js' @@ -64,7 +65,10 @@ const DataPermissionLevels = ({ return (
setOpen(true)} aria-haspopup="dialog"> - {triggerText} + + {triggerText} + + & { maxWidth?: string } diff --git a/packages/ui-instructure/src/DataPermissionLevels/v2/styles.ts b/packages/ui-instructure/src/DataPermissionLevels/v2/styles.ts index 40d9e11dbc..7dc52f97d1 100644 --- a/packages/ui-instructure/src/DataPermissionLevels/v2/styles.ts +++ b/packages/ui-instructure/src/DataPermissionLevels/v2/styles.ts @@ -22,8 +22,11 @@ * SOFTWARE. */ -import type { NewComponentTypes } from '@instructure/ui-themes' -import type { DataPermissionLevelsStyle } from './props' +import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes' +import type { + DataPermissionLevelsProps, + DataPermissionLevelsStyle +} from './props' /** * --- @@ -31,15 +34,25 @@ import type { DataPermissionLevelsStyle } from './props' * --- * Generates the style object from the theme and provided additional information * @param componentTheme The theme variable object. + * @param _props the props of the component, the style is applied to + * @param sharedTokens Shared token object that stores common values for the theme. * @return The final style object, which will be used in the component */ const generateStyle = ( - componentTheme: ReturnType + componentTheme: ReturnType, + _props: DataPermissionLevelsProps, + sharedTokens: SharedTokens ): DataPermissionLevelsStyle => { return { dataPermissionLevels: { label: 'data-permission-levels' }, + trigger: { + label: 'data-permission-levels__trigger', + display: 'inline-flex', + alignItems: 'center', + gap: sharedTokens?.spacing.general.spaceXs + }, body: { label: 'data-permission-levels__body', display: 'flex', diff --git a/packages/ui-instructure/src/NutritionFacts/__tests__/NutritionFacts.test.tsx b/packages/ui-instructure/src/NutritionFacts/__tests__/NutritionFacts.test.tsx new file mode 100644 index 0000000000..14bf1635c4 --- /dev/null +++ b/packages/ui-instructure/src/NutritionFacts/__tests__/NutritionFacts.test.tsx @@ -0,0 +1,81 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { render } from 'vitest-browser-react' +import { describe, it, expect } from 'vitest' +import { NutritionFacts } from '@instructure/ui-instructure/latest' + +describe('', () => { + const props = { + modalLabel: 'Nutrition facts modal', + title: 'AI Nutrition Facts', + featureName: 'Feature name', + closeButtonText: 'Close', + closeIconButtonScreenReaderLabel: 'Close', + triggerText: 'AI Nutrition Facts', + data: [ + { + blockTitle: 'Model & Data', + segmentData: [ + { + segmentTitle: 'Base Model', + description: 'The foundational AI model.', + value: 'GPT-4' + } + ] + } + ] + } + + describe('trigger', () => { + it('should render as a button that announces it opens a dialog', async () => { + await render() + + const trigger = document.querySelector('button') + + expect(trigger).toBeInTheDocument() + expect(trigger).toHaveAttribute('aria-haspopup', 'dialog') + expect(trigger).toHaveTextContent(props.triggerText) + }) + + it('should render an icon after the trigger text', async () => { + await render() + + const icon = document.querySelector('button svg') + + expect(icon).toBeInTheDocument() + // the icon is a suffix cue, so the label text must directly precede it + expect(icon!.parentElement!.previousSibling?.textContent).toBe( + props.triggerText + ) + }) + + it('should keep the icon decorative so it does not alter the accessible name', async () => { + await render() + + const icon = document.querySelector('button svg') + + expect(icon).toHaveAttribute('aria-hidden', 'true') + }) + }) +}) diff --git a/packages/ui-instructure/src/NutritionFacts/v2/index.tsx b/packages/ui-instructure/src/NutritionFacts/v2/index.tsx index 2eba516d87..d41d783d4c 100644 --- a/packages/ui-instructure/src/NutritionFacts/v2/index.tsx +++ b/packages/ui-instructure/src/NutritionFacts/v2/index.tsx @@ -30,6 +30,7 @@ import { } from '@instructure/ui-buttons/latest' import { Heading } from '@instructure/ui-heading/latest' import { Text } from '@instructure/ui-text/latest' +import { InfoInstUIIcon } from '@instructure/ui-icons' import { useStyleNew } from '@instructure/emotion' import { NutritionFactsProps } from './props.js' @@ -63,7 +64,10 @@ const NutritionFacts = ({ return (
setOpen(true)} aria-haspopup="dialog"> - {triggerText} + + {triggerText} + + & { maxWidth?: string } diff --git a/packages/ui-instructure/src/NutritionFacts/v2/styles.ts b/packages/ui-instructure/src/NutritionFacts/v2/styles.ts index 7b0f0abfd7..048b69c7c4 100644 --- a/packages/ui-instructure/src/NutritionFacts/v2/styles.ts +++ b/packages/ui-instructure/src/NutritionFacts/v2/styles.ts @@ -22,8 +22,8 @@ * SOFTWARE. */ -import type { NewComponentTypes } from '@instructure/ui-themes' -import type { NutritionFactsStyle } from './props' +import type { NewComponentTypes, SharedTokens } from '@instructure/ui-themes' +import type { NutritionFactsProps, NutritionFactsStyle } from './props' /** * --- @@ -31,12 +31,22 @@ import type { NutritionFactsStyle } from './props' * --- * Generates the style object from the theme and provided additional information * @param componentTheme The theme variable object. + * @param _props the props of the component, the style is applied to + * @param sharedTokens Shared token object that stores common values for the theme. * @return The final style object, which will be used in the component */ const generateStyle = ( - componentTheme: ReturnType + componentTheme: ReturnType, + _props: NutritionFactsProps, + sharedTokens: SharedTokens ): NutritionFactsStyle => { return { + trigger: { + label: 'nutrition-facts__trigger', + display: 'inline-flex', + alignItems: 'center', + gap: sharedTokens?.spacing.general.spaceXs + }, segmentCard: { label: 'nutrition-facts__segment-card', borderStyle: 'solid', From 2eb7c63801373fbb703b7898db8c30e2d78adb40 Mon Sep 17 00:00:00 2001 From: Nandor_Czegledi Date: Thu, 3 Sep 2026 09:14:43 +0200 Subject: [PATCH 2/2] test(ui-instructure): remove NutritionFacts and DataPermissionLevels unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) --- .../__tests__/DataPermissionLevels.test.tsx | 78 ------------------ .../__tests__/NutritionFacts.test.tsx | 81 ------------------- 2 files changed, 159 deletions(-) delete mode 100644 packages/ui-instructure/src/DataPermissionLevels/__tests__/DataPermissionLevels.test.tsx delete mode 100644 packages/ui-instructure/src/NutritionFacts/__tests__/NutritionFacts.test.tsx diff --git a/packages/ui-instructure/src/DataPermissionLevels/__tests__/DataPermissionLevels.test.tsx b/packages/ui-instructure/src/DataPermissionLevels/__tests__/DataPermissionLevels.test.tsx deleted file mode 100644 index 2d246dad09..0000000000 --- a/packages/ui-instructure/src/DataPermissionLevels/__tests__/DataPermissionLevels.test.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import { render } from 'vitest-browser-react' -import { describe, it, expect } from 'vitest' -import { DataPermissionLevels } from '@instructure/ui-instructure/latest' - -describe('', () => { - const props = { - modalLabel: 'Data permission levels modal', - title: 'Data Permission Levels', - currentFeatureText: 'Current Feature:', - currentFeature: 'Feature name', - closeButtonText: 'Close', - closeIconButtonScreenReaderLabel: 'Close', - triggerText: 'Permission Levels', - data: [ - { - level: 'LEVEL 1', - title: 'Descriptive Analytics and Research', - description: 'We leverage anonymized aggregate data.', - highlighted: true - } - ] - } - - describe('trigger', () => { - it('should render as a button that announces it opens a dialog', async () => { - await render() - - const trigger = document.querySelector('button') - - expect(trigger).toBeInTheDocument() - expect(trigger).toHaveAttribute('aria-haspopup', 'dialog') - expect(trigger).toHaveTextContent(props.triggerText) - }) - - it('should render an icon after the trigger text', async () => { - await render() - - const icon = document.querySelector('button svg') - - expect(icon).toBeInTheDocument() - // the icon is a suffix cue, so the label text must directly precede it - expect(icon!.parentElement!.previousSibling?.textContent).toBe( - props.triggerText - ) - }) - - it('should keep the icon decorative so it does not alter the accessible name', async () => { - await render() - - const icon = document.querySelector('button svg') - - expect(icon).toHaveAttribute('aria-hidden', 'true') - }) - }) -}) diff --git a/packages/ui-instructure/src/NutritionFacts/__tests__/NutritionFacts.test.tsx b/packages/ui-instructure/src/NutritionFacts/__tests__/NutritionFacts.test.tsx deleted file mode 100644 index 14bf1635c4..0000000000 --- a/packages/ui-instructure/src/NutritionFacts/__tests__/NutritionFacts.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2015 - present Instructure, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -import { render } from 'vitest-browser-react' -import { describe, it, expect } from 'vitest' -import { NutritionFacts } from '@instructure/ui-instructure/latest' - -describe('', () => { - const props = { - modalLabel: 'Nutrition facts modal', - title: 'AI Nutrition Facts', - featureName: 'Feature name', - closeButtonText: 'Close', - closeIconButtonScreenReaderLabel: 'Close', - triggerText: 'AI Nutrition Facts', - data: [ - { - blockTitle: 'Model & Data', - segmentData: [ - { - segmentTitle: 'Base Model', - description: 'The foundational AI model.', - value: 'GPT-4' - } - ] - } - ] - } - - describe('trigger', () => { - it('should render as a button that announces it opens a dialog', async () => { - await render() - - const trigger = document.querySelector('button') - - expect(trigger).toBeInTheDocument() - expect(trigger).toHaveAttribute('aria-haspopup', 'dialog') - expect(trigger).toHaveTextContent(props.triggerText) - }) - - it('should render an icon after the trigger text', async () => { - await render() - - const icon = document.querySelector('button svg') - - expect(icon).toBeInTheDocument() - // the icon is a suffix cue, so the label text must directly precede it - expect(icon!.parentElement!.previousSibling?.textContent).toBe( - props.triggerText - ) - }) - - it('should keep the icon decorative so it does not alter the accessible name', async () => { - await render() - - const icon = document.querySelector('button svg') - - expect(icon).toHaveAttribute('aria-hidden', 'true') - }) - }) -})