diff --git a/.changeset/techdocs-quick-owls-smile.md b/.changeset/techdocs-quick-owls-smile.md
new file mode 100644
index 0000000000..db4202fb5b
--- /dev/null
+++ b/.changeset/techdocs-quick-owls-smile.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-techdocs-module-addons-contrib': minor
+---
+
+A package for contributed TechDocs addons.
+
+In this release it will introduce the ReportIssue addon, which lets you select text and open a GitHub/Gitlab issue.
diff --git a/packages/app/package.json b/packages/app/package.json
index 6040c80698..aef8505bc0 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -55,6 +55,8 @@
"@backstage/plugin-stack-overflow": "^0.1.0",
"@backstage/plugin-tech-radar": "^0.5.11",
"@backstage/plugin-techdocs": "^1.1.0",
+ "@backstage/plugin-techdocs-react": "^0.1.0",
+ "@backstage/plugin-techdocs-module-addons-contrib": "^0.0.0",
"@backstage/plugin-todo": "^0.2.6",
"@backstage/plugin-user-settings": "^0.4.3",
"@backstage/plugin-tech-insights": "^0.2.0",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index ac0b9daa52..4cf2c02e55 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -69,6 +69,8 @@ import {
TechDocsReaderPage,
techdocsPlugin,
} from '@backstage/plugin-techdocs';
+import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
+import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
import {
UserSettingsPage,
UserSettingsTab,
@@ -179,6 +181,9 @@ const routes = (
element={}
>
{techDocsPage}
+
+
+
Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackstagePlugin } from '@backstage/core-plugin-api';
+import { ComponentType } from 'react';
+
+// @public
+export const ReportIssue: ComponentType;
+
+// @public (undocumented)
+export type ReportIssueProps = {
+ debounceTime?: number;
+ templateBuilder?: ReportIssueTemplateBuilder;
+};
+
+// @public (undocumented)
+export type ReportIssueTemplate = {
+ title: string;
+ body: string;
+};
+
+// @public (undocumented)
+export type ReportIssueTemplateBuilder = ({
+ selection,
+}: {
+ selection: Selection;
+}) => ReportIssueTemplate;
+
+// @public
+export const techdocsModuleAddonsContribPlugin: BackstagePlugin<{}, {}>;
+```
diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json
new file mode 100644
index 0000000000..c9695c40e2
--- /dev/null
+++ b/plugins/techdocs-module-addons-contrib/package.json
@@ -0,0 +1,69 @@
+{
+ "name": "@backstage/plugin-techdocs-module-addons-contrib",
+ "description": "Plugin module for contributed TechDocs Addons",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": false,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin-module"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/techdocs-module-addons-contrib"
+ },
+ "keywords": [
+ "backstage",
+ "techdocs"
+ ],
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/core-components": "^0.9.3",
+ "@backstage/core-plugin-api": "^1.0.1",
+ "@backstage/integration": "^1.1.0",
+ "@backstage/integration-react": "^1.0.1",
+ "@backstage/plugin-techdocs-react": "^0.1.0",
+ "@backstage/theme": "^0.2.15",
+ "@material-ui/core": "^4.9.13",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "4.0.0-alpha.57",
+ "git-url-parse": "^11.6.0",
+ "react-use": "^17.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.13.1 || ^17.0.0",
+ "react": "^16.13.1 || ^17.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.17.0",
+ "@backstage/core-app-api": "^1.0.1",
+ "@backstage/dev-utils": "^1.0.1",
+ "@backstage/test-utils": "^1.0.1",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/jest": "^26.0.7",
+ "@types/node": "^16.11.26",
+ "msw": "^0.35.0",
+ "cross-fetch": "^3.1.5"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx
new file mode 100644
index 0000000000..5fff56ecb7
--- /dev/null
+++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+
+import { analyticsApiRef } from '@backstage/core-plugin-api';
+import {
+ MockAnalyticsApi,
+ TestApiProvider,
+ wrapInTestApp,
+} from '@backstage/test-utils';
+
+import { IssueLink } from './IssueLink';
+
+const defaultProps = {
+ repository: {
+ type: 'github',
+ name: 'backstage',
+ owner: 'backstage',
+ protocol: 'https',
+ resource: 'github.com',
+ },
+ template: {
+ title: 'Documentation feedback',
+ body: '## Documentation Feedback 📝',
+ },
+};
+
+describe('FeedbackLink', () => {
+ const apiSpy = new MockAnalyticsApi();
+
+ it('Should open new issue tab', () => {
+ render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+
+ const link = screen.getByText(/Open new Github issue/);
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute('target', '_blank');
+ const encodedTitle = encodeURIComponent(defaultProps.template.title);
+ const encodedBody = encodeURIComponent(defaultProps.template.body);
+ expect(link).toHaveAttribute(
+ 'href',
+ `https://github.com/backstage/backstage/issues/new?title=${encodedTitle}&body=${encodedBody}`,
+ );
+ });
+
+ it('Should track click events', async () => {
+ render(
+ wrapInTestApp(
+
+
+ ,
+ ),
+ );
+
+ fireEvent.click(screen.getByText(/Open new Github issue/));
+
+ await waitFor(() => {
+ expect(apiSpy.getEvents()[0]).toMatchObject({
+ action: 'click',
+ subject: 'Open new Github issue',
+ });
+ });
+ });
+});
diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx
new file mode 100644
index 0000000000..0a1a64218a
--- /dev/null
+++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+
+import { makeStyles } from '@material-ui/core';
+import BugReportIcon from '@material-ui/icons/BugReport';
+
+import { Link, GitHubIcon } from '@backstage/core-components';
+
+import { ReportIssueTemplate, Repository } from './types';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ display: 'grid',
+ gridGap: theme.spacing(1),
+ gridAutoFlow: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ color: theme.palette.common.black,
+ fontSize: theme.typography.button.fontSize,
+ },
+}));
+
+type IssueLinkProps = {
+ template: ReportIssueTemplate;
+ repository: Repository;
+};
+
+const getIcon = ({ type }: Repository) => {
+ if (type === 'github') {
+ return GitHubIcon;
+ }
+ return BugReportIcon;
+};
+
+const getName = ({ type }: Repository) => {
+ if (type === 'github') {
+ return 'Github';
+ }
+ return 'Gitlab';
+};
+
+const getUrl = (repository: Repository, template: ReportIssueTemplate) => {
+ const { title, body } = template;
+ const encodedTitle = encodeURIComponent(title);
+ const encodedBody = encodeURIComponent(body);
+ const { protocol, resource, owner, name, type } = repository;
+ const encodedOwner = encodeURIComponent(owner);
+ const encodedName = encodeURIComponent(name);
+
+ const url = `${protocol}://${resource}/${encodedOwner}/${encodedName}`;
+ if (type === 'github') {
+ return `${url}/issues/new?title=${encodedTitle}&body=${encodedBody}`;
+ }
+ return `${url}/issues/new?[title]=${encodedTitle}&[body]=${encodedBody}`;
+};
+
+export const IssueLink = ({ template, repository }: IssueLinkProps) => {
+ const classes = useStyles();
+
+ const Icon = getIcon(repository);
+ const url = getUrl(repository, template);
+
+ return (
+
+ Open new {getName(repository)} issue
+
+ );
+};
diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.tsx
new file mode 100644
index 0000000000..2fe8d604e6
--- /dev/null
+++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.tsx
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useState, useEffect } from 'react';
+
+import { makeStyles, Portal, Paper } from '@material-ui/core';
+
+import { useGitTemplate, useGitRepository } from './hooks';
+import { ReportIssueTemplateBuilder } from './types';
+import {
+ PAGE_MAIN_CONTENT_SELECTOR,
+ PAGE_FEEDBACK_LINK_SELECTOR,
+ ADDON_FEEDBACK_CONTAINER_ID,
+ ADDON_FEEDBACK_CONTAINER_SELECTOR,
+} from './constants';
+import { IssueLink } from './IssueLink';
+
+import {
+ useShadowRootElements,
+ useShadowRootSelection,
+} from '@backstage/plugin-techdocs-react';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ transform: 'translate(-100%, -100%)',
+ position: 'absolute',
+ padding: theme.spacing(1),
+ zIndex: theme.zIndex.tooltip,
+ background: theme.palette.common.white,
+ },
+}));
+
+type Style = {
+ top: string;
+ left: string;
+};
+
+/**
+ * @public
+ */
+export type ReportIssueProps = {
+ debounceTime?: number;
+ templateBuilder?: ReportIssueTemplateBuilder;
+};
+
+/**
+ * Show report issue button when text is highlighted
+ */
+export const ReportIssueAddon = ({
+ debounceTime = 500,
+ templateBuilder: buildTemplate,
+}: ReportIssueProps) => {
+ const classes = useStyles();
+ const [style, setStyle] = useState