Skip to content

Commit 1c40b81

Browse files
mikethemandi
andauthored
refactor: replace custom clipboard with stimulus (pypi#13353)
* refactor: replace custom clipboard with stimulus Replaces the `clipboard` dependency with a Stimulus controller. Uses native `navigator` browser API. Refs: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText Signed-off-by: Mike Fiedler <[email protected]> * translations Signed-off-by: Mike Fiedler <[email protected]> --------- Signed-off-by: Mike Fiedler <[email protected]> Co-authored-by: Dustin Ingram <[email protected]>
1 parent 0a9f904 commit 1c40b81

File tree

11 files changed

+198
-114
lines changed

11 files changed

+198
-114
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* Licensed under the Apache License, Version 2.0 (the "License");
2+
* you may not use this file except in compliance with the License.
3+
* You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
/* global expect, beforeEach, describe, it, jest */
15+
16+
import { Application } from "@hotwired/stimulus";
17+
import ClipboardController from "../../warehouse/static/js/warehouse/controllers/clipboard_controller";
18+
19+
// Create a mock clipboard object, since jsdom doesn't support the clipboard API
20+
// See https://github.com/jsdom/jsdom/issues/1568
21+
Object.defineProperty(navigator, "clipboard", {
22+
writable: true,
23+
value: {
24+
writeText: jest.fn(),
25+
},
26+
});
27+
28+
29+
const clipboardContent = `
30+
<div data-controller="clipboard">
31+
<span id="#id" data-clipboard-target="source">Copyable Thing</span>
32+
<button
33+
type="button"
34+
class="copy-tooltip"
35+
data-action="clipboard#copy"
36+
data-clipboard-target="tooltip"
37+
data-clipboard-tooltip-value="Copy to clipboard"
38+
>
39+
<i class="fa fa-copy" aria-hidden="true"></i>
40+
</button>
41+
</div>
42+
`;
43+
44+
describe("ClipboardController", () => {
45+
beforeEach(() => {
46+
document.body.innerHTML = clipboardContent;
47+
const application = Application.start();
48+
application.register("clipboard", ClipboardController);
49+
});
50+
51+
describe("copy", () => {
52+
it("copies text to clipboard and resets", () => {
53+
const button = document.querySelector(".copy-tooltip");
54+
expect(button.dataset.clipboardTooltipValue).toEqual("Copy to clipboard");
55+
56+
button.click();
57+
58+
// Check that the text was copied to the clipboard
59+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Copyable Thing");
60+
// Check that the tooltip text was changed
61+
expect(button.dataset.clipboardTooltipValue).toEqual("Copied");
62+
63+
button.dispatchEvent(new FocusEvent("focusout"));
64+
65+
// Check that the tooltip text was reset
66+
expect(button.dataset.clipboardTooltipValue).toEqual("Copy to clipboard");
67+
});
68+
});
69+
});

warehouse/locale/messages.pot

+49-49
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,8 @@ msgstr ""
573573
#: warehouse/templates/manage/project/release.html:194
574574
#: warehouse/templates/manage/project/releases.html:140
575575
#: warehouse/templates/manage/project/releases.html:179
576-
#: warehouse/templates/packaging/detail.html:362
577-
#: warehouse/templates/packaging/detail.html:381
576+
#: warehouse/templates/packaging/detail.html:364
577+
#: warehouse/templates/packaging/detail.html:383
578578
#: warehouse/templates/pages/classifiers.html:25
579579
#: warehouse/templates/pages/help.html:20
580580
#: warehouse/templates/pages/help.html:218
@@ -2431,9 +2431,9 @@ msgstr ""
24312431
#: warehouse/templates/includes/hash-modal.html:50
24322432
#: warehouse/templates/includes/hash-modal.html:59
24332433
#: warehouse/templates/manage/account.html:206
2434-
#: warehouse/templates/manage/account/recovery_codes-provision.html:57
2434+
#: warehouse/templates/manage/account/recovery_codes-provision.html:58
24352435
#: warehouse/templates/manage/account/totp-provision.html:57
2436-
#: warehouse/templates/packaging/detail.html:161
2436+
#: warehouse/templates/packaging/detail.html:162
24372437
#: warehouse/templates/pages/classifiers.html:38
24382438
msgid "Copy to clipboard"
24392439
msgstr ""
@@ -2442,7 +2442,7 @@ msgstr ""
24422442
#: warehouse/templates/includes/hash-modal.html:51
24432443
#: warehouse/templates/includes/hash-modal.html:60
24442444
#: warehouse/templates/manage/account.html:207
2445-
#: warehouse/templates/manage/account/recovery_codes-provision.html:58
2445+
#: warehouse/templates/manage/account/recovery_codes-provision.html:59
24462446
#: warehouse/templates/manage/account/totp-provision.html:58
24472447
#: warehouse/templates/pages/classifiers.html:39
24482448
msgid "Copy"
@@ -4055,26 +4055,26 @@ msgstr ""
40554055
msgid "Save your recovery codes"
40564056
msgstr ""
40574057

4058-
#: warehouse/templates/manage/account/recovery_codes-provision.html:62
4058+
#: warehouse/templates/manage/account/recovery_codes-provision.html:63
40594059
msgid "Download as file"
40604060
msgstr ""
40614061

4062-
#: warehouse/templates/manage/account/recovery_codes-provision.html:63
4062+
#: warehouse/templates/manage/account/recovery_codes-provision.html:64
40634063
#: warehouse/templates/manage/organization/roles.html:193
40644064
#: warehouse/templates/manage/project/roles.html:134
40654065
#: warehouse/templates/manage/project/roles.html:191
40664066
msgid "Save"
40674067
msgstr ""
40684068

4069-
#: warehouse/templates/manage/account/recovery_codes-provision.html:66
4069+
#: warehouse/templates/manage/account/recovery_codes-provision.html:67
40704070
msgid "Continue"
40714071
msgstr ""
40724072

4073-
#: warehouse/templates/manage/account/recovery_codes-provision.html:69
4073+
#: warehouse/templates/manage/account/recovery_codes-provision.html:70
40744074
msgid "These codes will not be visible again."
40754075
msgstr ""
40764076

4077-
#: warehouse/templates/manage/account/recovery_codes-provision.html:72
4077+
#: warehouse/templates/manage/account/recovery_codes-provision.html:73
40784078
msgid "Ensure that you have securely stored them before continuing."
40794079
msgstr ""
40804080

@@ -6014,139 +6014,139 @@ msgstr ""
60146014
msgid "RSS: latest releases for %(project_name)s"
60156015
msgstr ""
60166016

6017-
#: warehouse/templates/packaging/detail.html:163
6017+
#: warehouse/templates/packaging/detail.html:164
60186018
msgid "Copy PIP instructions"
60196019
msgstr ""
60206020

6021-
#: warehouse/templates/packaging/detail.html:173
6021+
#: warehouse/templates/packaging/detail.html:175
60226022
msgid "This release has been yanked"
60236023
msgstr ""
60246024

6025-
#: warehouse/templates/packaging/detail.html:179
6025+
#: warehouse/templates/packaging/detail.html:181
60266026
#, python-format
60276027
msgid "Stable version available (%(version)s)"
60286028
msgstr ""
60296029

6030-
#: warehouse/templates/packaging/detail.html:183
6030+
#: warehouse/templates/packaging/detail.html:185
60316031
#, python-format
60326032
msgid "Newer version available (%(version)s)"
60336033
msgstr ""
60346034

6035-
#: warehouse/templates/packaging/detail.html:187
6035+
#: warehouse/templates/packaging/detail.html:189
60366036
msgid "Latest version"
60376037
msgstr ""
60386038

6039-
#: warehouse/templates/packaging/detail.html:192
6039+
#: warehouse/templates/packaging/detail.html:194
60406040
#, python-format
60416041
msgid "Released: %(release_date)s"
60426042
msgstr ""
60436043

6044-
#: warehouse/templates/packaging/detail.html:204
6044+
#: warehouse/templates/packaging/detail.html:206
60456045
msgid "No project description provided"
60466046
msgstr ""
60476047

6048-
#: warehouse/templates/packaging/detail.html:217
6048+
#: warehouse/templates/packaging/detail.html:219
60496049
msgid "Navigation"
60506050
msgstr ""
60516051

6052-
#: warehouse/templates/packaging/detail.html:218
6053-
#: warehouse/templates/packaging/detail.html:250
6052+
#: warehouse/templates/packaging/detail.html:220
6053+
#: warehouse/templates/packaging/detail.html:252
60546054
#, python-format
60556055
msgid "Navigation for %(project)s"
60566056
msgstr ""
60576057

6058-
#: warehouse/templates/packaging/detail.html:221
6059-
#: warehouse/templates/packaging/detail.html:253
6058+
#: warehouse/templates/packaging/detail.html:223
6059+
#: warehouse/templates/packaging/detail.html:255
60606060
msgid "Project description. Focus will be moved to the description."
60616061
msgstr ""
60626062

6063-
#: warehouse/templates/packaging/detail.html:223
6064-
#: warehouse/templates/packaging/detail.html:255
6065-
#: warehouse/templates/packaging/detail.html:283
6063+
#: warehouse/templates/packaging/detail.html:225
6064+
#: warehouse/templates/packaging/detail.html:257
6065+
#: warehouse/templates/packaging/detail.html:285
60666066
msgid "Project description"
60676067
msgstr ""
60686068

6069-
#: warehouse/templates/packaging/detail.html:227
6070-
#: warehouse/templates/packaging/detail.html:265
6069+
#: warehouse/templates/packaging/detail.html:229
6070+
#: warehouse/templates/packaging/detail.html:267
60716071
msgid "Release history. Focus will be moved to the history panel."
60726072
msgstr ""
60736073

6074-
#: warehouse/templates/packaging/detail.html:229
6075-
#: warehouse/templates/packaging/detail.html:267
6076-
#: warehouse/templates/packaging/detail.html:305
6074+
#: warehouse/templates/packaging/detail.html:231
6075+
#: warehouse/templates/packaging/detail.html:269
6076+
#: warehouse/templates/packaging/detail.html:307
60776077
msgid "Release history"
60786078
msgstr ""
60796079

6080-
#: warehouse/templates/packaging/detail.html:234
6081-
#: warehouse/templates/packaging/detail.html:272
6080+
#: warehouse/templates/packaging/detail.html:236
6081+
#: warehouse/templates/packaging/detail.html:274
60826082
msgid "Download files. Focus will be moved to the project files."
60836083
msgstr ""
60846084

6085-
#: warehouse/templates/packaging/detail.html:236
6086-
#: warehouse/templates/packaging/detail.html:274
6087-
#: warehouse/templates/packaging/detail.html:361
6085+
#: warehouse/templates/packaging/detail.html:238
6086+
#: warehouse/templates/packaging/detail.html:276
6087+
#: warehouse/templates/packaging/detail.html:363
60886088
msgid "Download files"
60896089
msgstr ""
60906090

6091-
#: warehouse/templates/packaging/detail.html:259
6091+
#: warehouse/templates/packaging/detail.html:261
60926092
msgid "Project details. Focus will be moved to the project details."
60936093
msgstr ""
60946094

6095-
#: warehouse/templates/packaging/detail.html:261
6096-
#: warehouse/templates/packaging/detail.html:297
6095+
#: warehouse/templates/packaging/detail.html:263
6096+
#: warehouse/templates/packaging/detail.html:299
60976097
msgid "Project details"
60986098
msgstr ""
60996099

6100-
#: warehouse/templates/packaging/detail.html:290
6100+
#: warehouse/templates/packaging/detail.html:292
61016101
msgid "The author of this package has not provided a project description"
61026102
msgstr ""
61036103

6104-
#: warehouse/templates/packaging/detail.html:307
6104+
#: warehouse/templates/packaging/detail.html:309
61056105
msgid "Release notifications"
61066106
msgstr ""
61076107

6108-
#: warehouse/templates/packaging/detail.html:308
6108+
#: warehouse/templates/packaging/detail.html:310
61096109
msgid "RSS feed"
61106110
msgstr ""
61116111

6112-
#: warehouse/templates/packaging/detail.html:320
6112+
#: warehouse/templates/packaging/detail.html:322
61136113
msgid "This version"
61146114
msgstr ""
61156115

6116-
#: warehouse/templates/packaging/detail.html:340
6116+
#: warehouse/templates/packaging/detail.html:342
61176117
msgid "pre-release"
61186118
msgstr ""
61196119

6120-
#: warehouse/templates/packaging/detail.html:345
6120+
#: warehouse/templates/packaging/detail.html:347
61216121
msgid "yanked"
61226122
msgstr ""
61236123

6124-
#: warehouse/templates/packaging/detail.html:362
6124+
#: warehouse/templates/packaging/detail.html:364
61256125
#, python-format
61266126
msgid ""
61276127
"Download the file for your platform. If you're not sure which to choose, "
61286128
"learn more about <a href=\"%(href)s\" title=\"%(title)s\" "
61296129
"target=\"_blank\" rel=\"noopener\">installing packages</a>."
61306130
msgstr ""
61316131

6132-
#: warehouse/templates/packaging/detail.html:364
6132+
#: warehouse/templates/packaging/detail.html:366
61336133
msgid "Source Distribution"
61346134
msgid_plural "Source Distributions"
61356135
msgstr[0] ""
61366136
msgstr[1] ""
61376137

6138-
#: warehouse/templates/packaging/detail.html:380
6138+
#: warehouse/templates/packaging/detail.html:382
61396139
msgid "No source distribution files available for this release."
61406140
msgstr ""
61416141

6142-
#: warehouse/templates/packaging/detail.html:381
6142+
#: warehouse/templates/packaging/detail.html:383
61436143
#, python-format
61446144
msgid ""
61456145
"See tutorial on <a href=\"%(href)s\" title=\"%(title)s\" "
61466146
"target=\"_blank\" rel=\"noopener\">generating distribution archives</a>."
61476147
msgstr ""
61486148

6149-
#: warehouse/templates/packaging/detail.html:388
6149+
#: warehouse/templates/packaging/detail.html:390
61506150
msgid "Built Distribution"
61516151
msgid_plural "Built Distributions"
61526152
msgstr[0] ""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
import { Controller } from "@hotwired/stimulus";
15+
16+
// Copy handler for copy tooltips, e.g.
17+
// - the pip command on package detail page
18+
// - the copy hash on package detail page
19+
// - the copy hash on release maintainers page
20+
21+
// See `warehouse/static/sass/blocks/_copy-tooltip.scss` for style details.
22+
export default class extends Controller {
23+
static targets = [ "source", "tooltip" ];
24+
25+
copy() {
26+
// save the original tooltip text
27+
const clipboardTooltipOriginalValue = this.tooltipTarget.dataset.clipboardTooltipValue;
28+
// copy the source text to clipboard
29+
navigator.clipboard.writeText(this.sourceTarget.textContent);
30+
// set the tooltip text to "Copied"
31+
this.tooltipTarget.dataset.clipboardTooltipValue = "Copied";
32+
33+
// on focusout and mouseout, reset the tooltip text to the original value
34+
const resetTooltip = () => {
35+
// restore the original tooltip text
36+
this.tooltipTarget.dataset.clipboardTooltipValue = clipboardTooltipOriginalValue;
37+
// remove focus
38+
this.tooltipTarget.blur();
39+
// remove event listeners
40+
this.tooltipTarget.removeEventListener("focusout", resetTooltip);
41+
this.tooltipTarget.removeEventListener("mouseout", resetTooltip);
42+
};
43+
this.tooltipTarget.addEventListener("focusout", resetTooltip);
44+
this.tooltipTarget.addEventListener("mouseout", resetTooltip);
45+
}
46+
}

0 commit comments

Comments
 (0)