@cnamts/synapse 0.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -0
  3. package/dist/design-system-v3.d.ts +246 -0
  4. package/dist/design-system-v3.js +5425 -0
  5. package/dist/design-system-v3.umd.cjs +2 -0
  6. package/dist/style.css +1 -0
  7. package/package.json +104 -0
  8. package/src/assets/tokens.scss +500 -0
  9. package/src/components/Alert/Alert.mdx +36 -0
  10. package/src/components/Alert/Alert.stories.ts +115 -0
  11. package/src/components/Alert/Alert.vue +248 -0
  12. package/src/components/Alert/locales.ts +3 -0
  13. package/src/components/Alert/tests/Alert.spec.ts +105 -0
  14. package/src/components/Alert/tests/__snapshots__/Alert.spec.ts.snap +15 -0
  15. package/src/components/BackBtn/BackBtn.mdx +26 -0
  16. package/src/components/BackBtn/BackBtn.stories.ts +138 -0
  17. package/src/components/BackBtn/BackBtn.vue +60 -0
  18. package/src/components/BackBtn/locales.ts +3 -0
  19. package/src/components/BackBtn/tests/BackBtn.spec.ts +103 -0
  20. package/src/components/BackBtn/tests/__snapshots__/BackBtn.spec.ts.snap +9 -0
  21. package/src/components/BackToTopBtn/BackToTopBtn.mdx +52 -0
  22. package/src/components/BackToTopBtn/BackToTopBtn.stories.ts +188 -0
  23. package/src/components/BackToTopBtn/BackToTopBtn.vue +137 -0
  24. package/src/components/BackToTopBtn/config.ts +12 -0
  25. package/src/components/BackToTopBtn/locales.ts +3 -0
  26. package/src/components/BackToTopBtn/tests/BackToTopBtn.spec.ts +173 -0
  27. package/src/components/BackToTopBtn/tests/__snapshots__/BackToTopBtn.spec.ts.snap +17 -0
  28. package/src/components/Beta/beta.mdx +5 -0
  29. package/src/components/CopyBtn/CopyBtn.mdx +38 -0
  30. package/src/components/CopyBtn/CopyBtn.stories.ts +209 -0
  31. package/src/components/CopyBtn/CopyBtn.vue +103 -0
  32. package/src/components/CopyBtn/config.ts +17 -0
  33. package/src/components/CopyBtn/locales.ts +3 -0
  34. package/src/components/CopyBtn/tests/CopyBtn.spec.ts +99 -0
  35. package/src/components/CopyBtn/tests/__snapshots__/CopyBtn.spec.ts.snap +7 -0
  36. package/src/components/Deprecated/deprecated.mdx +5 -0
  37. package/src/components/DownloadBtn/DownloadBtn.mdx +94 -0
  38. package/src/components/DownloadBtn/DownloadBtn.stories.ts +211 -0
  39. package/src/components/DownloadBtn/DownloadBtn.vue +113 -0
  40. package/src/components/DownloadBtn/config.ts +13 -0
  41. package/src/components/DownloadBtn/tests/DownloadBtn.spec.ts +82 -0
  42. package/src/components/DownloadBtn/tests/__snapshots__/DownloadBtn.spec.ts.snap +17 -0
  43. package/src/components/DownloadBtn/tests/data/filePromise.ts +53 -0
  44. package/src/components/DownloadBtn/tests/data/test.json +0 -0
  45. package/src/components/FranceConnectBtn/FranceConnectBtn.mdx +34 -0
  46. package/src/components/FranceConnectBtn/FranceConnectBtn.stories.ts +92 -0
  47. package/src/components/FranceConnectBtn/FranceConnectBtn.vue +154 -0
  48. package/src/components/FranceConnectBtn/locales.ts +6 -0
  49. package/src/components/FranceConnectBtn/tests/FranceConnectBtn.spec.ts +62 -0
  50. package/src/components/FranceConnectBtn/tests/__snapshots__/FranceConnectBtn.spec.ts.snap +36 -0
  51. package/src/components/LangBtn/LangBtn.mdx +37 -0
  52. package/src/components/LangBtn/LangBtn.stories.ts +147 -0
  53. package/src/components/LangBtn/LangBtn.vue +167 -0
  54. package/src/components/LangBtn/config.ts +17 -0
  55. package/src/components/LangBtn/locales.ts +3 -0
  56. package/src/components/LangBtn/tests/Config.spec.ts +24 -0
  57. package/src/components/LangBtn/tests/LangBtn.spec.ts +283 -0
  58. package/src/components/LangBtn/tests/__snapshots__/LangBtn.spec.ts.snap +11 -0
  59. package/src/components/LangBtn/types.d.ts +7 -0
  60. package/src/components/NotificationBar/NotificationBar.mdx +94 -0
  61. package/src/components/NotificationBar/NotificationBar.stories.ts +366 -0
  62. package/src/components/NotificationBar/NotificationBar.vue +296 -0
  63. package/src/components/NotificationBar/options.ts +15 -0
  64. package/src/components/NotificationBar/tests/NotificationBar.spec.ts +332 -0
  65. package/src/components/NotificationBar/tests/__snapshots__/NotificationBar.spec.ts.snap +7 -0
  66. package/src/components/NotificationBar/types.ts +7 -0
  67. package/src/components/PageContainer/PageContainer.mdx +29 -0
  68. package/src/components/PageContainer/PageContainer.stories.ts +115 -0
  69. package/src/components/PageContainer/PageContainer.vue +68 -0
  70. package/src/components/PageContainer/tests/PageContainer.spec.ts +56 -0
  71. package/src/components/PageContainer/tests/__snapshots__/PageContainer.spec.ts.snap +7 -0
  72. package/src/components/SkipLink/SkipLink.mdx +55 -0
  73. package/src/components/SkipLink/SkipLink.stories.ts +70 -0
  74. package/src/components/SkipLink/SkipLink.vue +79 -0
  75. package/src/components/SkipLink/locales.ts +3 -0
  76. package/src/components/SkipLink/tests/__snapshots__/skipLink.spec.ts.snap +3 -0
  77. package/src/components/SkipLink/tests/skipLink.spec.ts +46 -0
  78. package/src/components/index.ts +8 -0
  79. package/src/composables/useCustomizableOptions.ts +23 -0
  80. package/src/designTokens/bootstrapColors.md +66 -0
  81. package/src/designTokens/cnamColors.md +193 -0
  82. package/src/designTokens/index.ts +15 -0
  83. package/src/designTokens/tokens/bootstrap/bootstrapColors.ts +158 -0
  84. package/src/designTokens/tokens/bootstrap/bootstrapLightTheme.ts +22 -0
  85. package/src/designTokens/tokens/cnam/cnamColors.ts +171 -0
  86. package/src/designTokens/tokens/cnam/cnamContextual.ts +58 -0
  87. package/src/designTokens/tokens/cnam/cnamLightTheme.ts +90 -0
  88. package/src/designTokens/tokens/cnam/cnamSemantic.ts +87 -0
  89. package/src/designTokens/tokens/json/contextual-tokens.json +156 -0
  90. package/src/designTokens/tokens/json/primitives.json +209 -0
  91. package/src/designTokens/tokens/json/semantic.json +120 -0
  92. package/src/designTokens/utils/convertGaps.ts +11 -0
  93. package/src/designTokens/utils/convertSemanticsToken.ts +32 -0
  94. package/src/designTokens/utils/createFlattenTheme.ts +19 -0
  95. package/src/designTokens/utils/index.ts +4 -0
  96. package/src/main.ts +2 -0
  97. package/src/services/NotificationService.ts +27 -0
  98. package/src/stories/Fondamentaux/Accessibilite/Accessibilite.mdx +52 -0
  99. package/src/stories/Fondamentaux/Accessibilite/Accessibilite.stories.ts +36 -0
  100. package/src/stories/Fondamentaux/Accessibilite/AccessibiliteItems.ts +706 -0
  101. package/src/stories/Fondamentaux/Accessibilite/constants/ExpertiseLevelEnum.ts +5 -0
  102. package/src/stories/Fondamentaux/Accessibilite/constants/RGAALevelEnum.ts +4 -0
  103. package/src/stories/Fondamentaux/EcoConception/EcoConception.mdx +24 -0
  104. package/src/stories/Fondamentaux/EcoConception/Econception.stories.ts +30 -0
  105. package/src/stories/Fondamentaux/EcoConception/ecoDesignItems.ts +55 -0
  106. package/src/stories/GuideDuDev/CommentContribuer.mdx +22 -0
  107. package/src/stories/GuideDuDev/components.stories.ts +23 -0
  108. package/src/stories/GuideDuDev/moduleDeNotification.mdx +182 -0
  109. package/src/stories/GuideDuDev/vuetifyOptions.mdx +72 -0
  110. package/src/stories/Guidelines/Colors.mdx +220 -0
  111. package/src/stories/Guidelines/CustomisationEtThemes.mdx +3 -0
  112. package/src/stories/Guidelines/Introduction.mdx +35 -0
  113. package/src/stories/Guidelines/Typo.mdx +53 -0
  114. package/src/stories/Home/Accueil.mdx +7 -0
  115. package/src/stories/Home/PolitiqueDeConfidentialite.mdx +4 -0
  116. package/src/stories/Home/synapse.webp +0 -0
  117. package/src/temp/TestA11y.vue +14 -0
  118. package/src/temp/TestComponent.vue +37 -0
  119. package/src/temp/TestDTComponent.vue +93 -0
  120. package/src/temp/customizableOptions.vue +18 -0
  121. package/src/temp/gridsTests.vue +54 -0
  122. package/src/temp/options.json +5 -0
  123. package/src/types/vuetifyTypes.ts +3 -0
  124. package/src/utils/convertToUnit/index.ts +16 -0
  125. package/src/utils/convertToUnit/test/convertToUnit.spec.ts +32 -0
  126. package/src/utils/functions/copyToClipboard/index.ts +38 -0
  127. package/src/utils/functions/copyToClipboard/tests/copyToClipboard.spec.ts +104 -0
  128. package/src/utils/functions/downloadFile/index.ts +37 -0
  129. package/src/utils/functions/downloadFile/tests/downloadFile.spec.ts +69 -0
  130. package/src/utils/functions/downloadFile/types.ts +1 -0
@@ -0,0 +1,35 @@
1
+ import { Meta } from '@storybook/addon-docs';
2
+
3
+ <Meta title="Guidelines/Introduction" />
4
+
5
+ Principes de design
6
+ ===================
7
+
8
+ Les principes éthiques de construction de notre Design System.
9
+
10
+ Être au service des usagers de la santé publique
11
+ ------------------------------------------------
12
+
13
+ En tant que Design System pour l'Assurance Maladie, notre devoir est de **répondre avec efficacité et intelligence aux besoins** de nos différents profils d'utilisateur citoyen.
14
+
15
+ Nous recommandons pour chaque utilisation de notre Design System de **favoriser l'écoute et l'empathie auprès des utilisateurs finaux** en récoltant dès que le projet le permet un ou plusieurs retours d'expériences. En suivant ce principe, c'est depuis 2017 plus de 20 produits et services numériques qui ont permis d'affiner et de structurer les choix ergonomiques de notre Design System.
16
+
17
+ Afin d'accompagner les équipes de l'Assurance Maladie, **nous pouvons aider à la mise en place de tests utilisateurs, au passage et à l'analyse des résultats**. En cohérence avec les besoins ou les contraintes des projets, nous vous conseillerons sur la typologie de test à adopter afin de répondre avec performance à vos besoins.
18
+
19
+ Assurer l'accessibilité pour tous
20
+ ---------------------------------
21
+
22
+ **L'accès aux produits numériques doit être permis à tous**. Pour cela, nous favorisons dans notre démarche de design le respect des [critères d'accessibilité](https://digital-design-system.netlify.app/fondamentaux/accessibilite) définis par le Référentiel Général d'Amélioration de l'Accessibilité.
23
+
24
+ Nous recommandons pour chaque utilisation de notre Design System de ne pas transformer les composants. La conception des composants vise à respecter quatre indices de performance. **Nos composants et leurs contenus informationnels sont perceptibles (1), manipulables (2), compréhensibles (3) et robustes (4) dans l'usage**. En complément, nous recommandons de proposer de la redondance informationnelle par un ou plusieurs moyens alternatifs lorsqu'une information est complexe à comprendre ou à atteindre sur l'interface.
25
+
26
+ Afin d'accompagner les équipes de l'Assurance Maladie, **nous pouvons aider à l'élaboration de vos interfaces pour qu'elles respectent les normes d'accessibilité**. Dans le cas où nos composants ne correspondent pas à vos exigences, nous pouvons collaborer avec vous à leurs adaptations.
27
+
28
+ Rendre évident ce qui est complexe
29
+ ----------------------------------
30
+
31
+ Malgré la complexité des projets, notre Design System doit permettre **la construction d'interfaces fluides et simples d'utilisation** pour assurer aux utilisateurs finaux une bonne expérience numérique.
32
+
33
+ Nous recommandons pour chaque utilisation de notre Design System de favoriser des **compositions graphiques d'interfaces peu chargées avec des espaces vierges et une hiérarchisation des informations**. Afin de permettre le responsive design, les interfaces doivent être structurées en utilisant des grilles et permettre ainsi l'affichage multi-support. En complément, nous recommandons de favoriser des parcours utilisateurs courts et de les concevoir en respectant pour chaque étape [la théorie de l'action proposée par Don Norman](https://fr.wikipedia.org/wiki/Th%C3%A9orie_de_l%27action_(Norman)).
34
+
35
+ Afin d'accompagner les équipes de l'Assurance Maladie, nous pouvons aider à structurer les interfaces. En proposant par exemple des ateliers de cadrage participatifs dont l'objectif est de relever auprès de vos besoins et des besoins des utilisateurs finaux les informations pertinentes (insights) à afficher. C'est une étape importante pour l'organisation graphique des interfaces.
@@ -0,0 +1,53 @@
1
+ import { Meta, Typeset } from '@storybook/addon-docs';
2
+
3
+ <Meta title="Guidelines/Typographie" />
4
+
5
+ export const typography = {
6
+ type: {
7
+ primary: 'Arial',
8
+ },
9
+ weight: {
10
+ regular: '400',
11
+ bold: '700',
12
+ extrabold: '800',
13
+ black: '900',
14
+ },
15
+ size: {
16
+ s1: 12,
17
+ s2: 14,
18
+ s3: 16,
19
+ m1: 18,
20
+ m2: 20,
21
+ m3: 24,
22
+ l1: 28,
23
+ l2: 30,
24
+ l3: 32,
25
+ l4:40,
26
+ },
27
+ };
28
+
29
+ export const SampleText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
30
+
31
+ # Typography
32
+
33
+ **Font:** Arial
34
+
35
+ **Weights:** 400(regular), 700(bold), 800(extrabold), 900(black)
36
+
37
+ <Typeset
38
+ fontSizes={[
39
+ Number(typography.size.s1),
40
+ Number(typography.size.s2),
41
+ Number(typography.size.s3),
42
+ Number(typography.size.m1),
43
+ Number(typography.size.m2),
44
+ Number(typography.size.m3),
45
+ Number(typography.size.l1),
46
+ Number(typography.size.l2),
47
+ Number(typography.size.l3),
48
+ Number(typography.size.l4),
49
+ ]}
50
+ fontWeight={typography.weight.black}
51
+ sampleText={SampleText}
52
+ fontFamily={typography.type.primary}
53
+ />
@@ -0,0 +1,7 @@
1
+ import { Meta } from '@storybook/addon-docs';
2
+ import synapse from './synapse.webp';
3
+
4
+ <Meta title="Home/Accueil" />
5
+
6
+ <img src={synapse} alt="photo de presentation du design system" style={{ maxWidth: '100%' }} />
7
+
@@ -0,0 +1,4 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Home/Politique de Confidentialité" />
4
+ # Politique de Confidentialité
Binary file
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <div>
6
+ <img
7
+ src="image.jpg"
8
+ alt="img"
9
+ >
10
+ <button>Click me</button>
11
+ </div>
12
+ </template>
13
+
14
+ <style lang="scss" scoped></style>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { VBtn } from 'vuetify/components/VBtn'
4
+
5
+ const props = withDefaults(defineProps<{
6
+ prop1?: string
7
+ number1: number
8
+ number2: number
9
+ }>(), {
10
+
11
+ prop1: 'default value',
12
+ })
13
+
14
+ defineEmits([
15
+ 'click',
16
+ ])
17
+
18
+ const value = defineModel< string >()
19
+ const result = computed(() => {
20
+ return props.number1 + props.number2
21
+ })
22
+ </script>
23
+
24
+ <template>
25
+ <div>
26
+ {{ prop1 }}
27
+ </div>
28
+ <v-btn @click="$emit('click', result)">
29
+ click me
30
+ </v-btn>
31
+ <div>
32
+ Result : {{ result }}
33
+ </div>
34
+ <div>{{ value }}</div>
35
+ </template>
36
+
37
+ <style lang="scss" scoped></style>
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <v-col
3
+ cols="2"
4
+ class="d-flex flex-column ga-3 mx-auto"
5
+ >
6
+ <h1 class="text-center">
7
+ Test Heading
8
+ </h1>
9
+ <VBtn color="primary">
10
+ Primary Button
11
+ </VBtn>
12
+ <VBtn color="secondary">
13
+ Secondary Button
14
+ </VBtn>
15
+ <VBtn color="accent">
16
+ Accent Button
17
+ </VBtn>
18
+ <VBtn color="warning">
19
+ Warning Button
20
+ </VBtn>
21
+ <VBtn color="success">
22
+ Success Button
23
+ </VBtn>
24
+ <VBtn color="error">
25
+ Error Button
26
+ </VBtn>
27
+ <VBtn color="info">
28
+ Info Button
29
+ </VBtn>
30
+ <VBtn color="risquePro">
31
+ RisquePro Button
32
+ </VBtn>
33
+ <VBtn color="backgroundAssure">
34
+ Assure Button
35
+ </VBtn>
36
+ <VBtn color="transparentBlue18">
37
+ TransparentBlue18 Button
38
+ </VBtn>
39
+ <VBtn color="transparentBlue8">
40
+ TransparentBlue8 Button
41
+ </VBtn>
42
+ <VBtn color="transparentBlue0">
43
+ TransparentBlue0 Button
44
+ </VBtn>
45
+ <VBtn color="toto">
46
+ Toto Button
47
+ </VBtn>
48
+ <div>
49
+ <div
50
+ class="border-xl"
51
+ style="height: 64px; width: 64px;"
52
+ >
53
+ <div class="text-caption">
54
+ border-xl
55
+ </div>
56
+ </div>
57
+ </div>
58
+ <div>
59
+ <v-card-title class="title">
60
+ Titre
61
+ </v-card-title>
62
+ <v-card-title class="alt-title">
63
+ Titre Alternatif
64
+ </v-card-title>
65
+ <v-card-text class="body-text">
66
+ Ceci est un corps de texte.
67
+ </v-card-text>
68
+ <v-btn class="link-label">
69
+ Lien ou libellé
70
+ </v-btn>
71
+ </div>
72
+ </v-col>
73
+ <div />
74
+ </template>
75
+
76
+ <style lang="scss" scoped>
77
+ <<<<<<<< HEAD:src/temp/TestDTComponent.vue
78
+ @import '../assets/tokens';
79
+ ========
80
+ @import '../../../assets/tokens';
81
+ >>>>>>>> 82f4bcd (chore(deps): update dependency vue to v3.5.2 (#66)):src/components/Temp/TestDesignTokensComponent/TestDTComponent.vue
82
+
83
+ h1 {
84
+ color: $orange-base;
85
+ }
86
+
87
+ button {
88
+ background-color: $blue-base;
89
+ color: $white-base;
90
+ border-radius: $radius-rounded;
91
+ //padding: $padding-10 $padding-4;
92
+ }
93
+ </style>
@@ -0,0 +1,18 @@
1
+ <script lang="ts" setup>
2
+ import useCustomizableOptions, { type CustomizableOptions } from '@/composables/useCustomizableOptions'
3
+ import defaultOptions from './options.json'
4
+
5
+ const props = defineProps<CustomizableOptions & {
6
+ title: string
7
+ }>()
8
+
9
+ const options = useCustomizableOptions(defaultOptions, props)
10
+ </script>
11
+
12
+ <template>
13
+ <div>
14
+ <VBtn v-bind="options.btn">
15
+ {{ title }}
16
+ </VBtn>
17
+ </div>
18
+ </template>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { reactive, ref, onMounted } from 'vue'
3
+ const state = reactive({
4
+ screenWidth: 0,
5
+ gridColumns: 1,
6
+ })
7
+ const calculateColumns = (width: number) => {
8
+ state.screenWidth = width
9
+ if (width >= 1200) {
10
+ state.gridColumns = 12
11
+ }
12
+ else if (width >= 900) {
13
+ state.gridColumns = 3
14
+ }
15
+ else if (width >= 600) {
16
+ state.gridColumns = 2
17
+ }
18
+ else {
19
+ state.gridColumns = 1
20
+ }
21
+ }
22
+ const targetElement = ref<HTMLElement | null>(null)
23
+
24
+ onMounted(() => {
25
+ targetElement.value = document.body
26
+ calculateColumns(targetElement.value.clientWidth)
27
+ })
28
+ </script>
29
+
30
+ <template>
31
+ <v-container>
32
+ <v-col>
33
+ <div class="bg-blue-accent-4 top-0 text-center">
34
+ Menu
35
+ </div>
36
+ <div class="page-container">
37
+ <v-col
38
+ v-for="n in 12"
39
+ :key="n"
40
+ class="bg-amber-darken-4"
41
+ >
42
+ {{ n }}
43
+ </v-col>
44
+ </div>
45
+ <div class="bg-blue-accent-4 top-0 text-center">
46
+ Footer
47
+ </div>
48
+ </v-col>
49
+ </v-container>
50
+ </template>
51
+
52
+ <style lang="scss" scoped>
53
+ @import '@/assets/tokens';
54
+ </style>
@@ -0,0 +1,5 @@
1
+ {
2
+ "btn": {
3
+ "color": "primary"
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ export type VariantType = 'text' | 'flat' | 'elevated' | 'tonal' | 'outlined' | 'plain'
2
+ export type DensityType = 'comfortable' | 'compact'
3
+ export type locationType = 'start' | 'end' | 'top' | 'bottom' | 'start center' | 'end center' | 'top center' | 'bottom center'
@@ -0,0 +1,16 @@
1
+ /** Convert a value to CSS unit */
2
+ export function convertToUnit(
3
+ str: string | number | null | undefined,
4
+ unit = 'px',
5
+ ): string | undefined {
6
+ if (str === undefined || str === null || str === '') {
7
+ return undefined
8
+ }
9
+ else if (isNaN(+str)) {
10
+ // If NaN, it's a string with unit, use as is
11
+ return String(str)
12
+ }
13
+ else {
14
+ return `${Number(str)}${unit}`
15
+ }
16
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { convertToUnit } from '../'
3
+
4
+ describe('convertToUnit', () => {
5
+ it('returns undefined when the value is null', () => {
6
+ expect(convertToUnit(null)).toBeUndefined()
7
+ })
8
+
9
+ it('returns undefined when the value is undefined', () => {
10
+ expect(convertToUnit(undefined)).toBeUndefined()
11
+ })
12
+
13
+ it('returns undefined when the value is an empty string', () => {
14
+ expect(convertToUnit('')).toBeUndefined()
15
+ })
16
+
17
+ it('returns the value when it is a string with unit', () => {
18
+ expect(convertToUnit('16px')).toBe('16px')
19
+ })
20
+
21
+ it('converts the value when it is a string without unit', () => {
22
+ expect(convertToUnit('16')).toBe('16px')
23
+ })
24
+
25
+ it('converts the value to a string when it is a number', () => {
26
+ expect(convertToUnit(16)).toBe('16px')
27
+ })
28
+
29
+ it('converts the value to a string with custom unit when it is a number', () => {
30
+ expect(convertToUnit(1, 'rem')).toBe('1rem')
31
+ })
32
+ })
@@ -0,0 +1,38 @@
1
+ /** Copy text to the clipboard */
2
+ export function copyToClipboard(textToCopy: string): void {
3
+ // Use a textarea, so we can use execCommand
4
+ const el = document.createElement('textarea')
5
+
6
+ el.value = textToCopy
7
+ el.setAttribute('readonly', '')
8
+ el.style.position = 'absolute'
9
+ el.style.left = '-9999px'
10
+
11
+ document.body.appendChild(el)
12
+
13
+ const selection = document.getSelection()
14
+
15
+ let selected: Range | false = false
16
+
17
+ if (selection) {
18
+ // Store previous selection (or false) to restore it after
19
+ selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false
20
+ }
21
+
22
+ el.select()
23
+
24
+ if (navigator.clipboard) {
25
+ navigator.clipboard.writeText(textToCopy)
26
+ }
27
+ else {
28
+ document.execCommand('copy')
29
+ }
30
+
31
+ document.body.removeChild(el)
32
+
33
+ // If a selection existed before copying
34
+ if (selection && selected) {
35
+ selection.removeAllRanges() // Unselect everything on the HTML document
36
+ selection.addRange(selected) // Restore the original selection
37
+ }
38
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest'
2
+ import { copyToClipboard } from '../'
3
+
4
+ interface TSelection {
5
+ rangeCount?: number
6
+ getRangeAt?: (index: number) => string | null
7
+ removeAllRanges?: () => null
8
+ addRange?: () => null
9
+ }
10
+
11
+ // Custom document type
12
+ interface TDocument {
13
+ getSelection: () => TSelection | null
14
+ execCommand: () => boolean
15
+ }
16
+
17
+ // Override default type
18
+ declare let document: TDocument
19
+
20
+ /** Mock functions on document */
21
+ function mockDocument(options: TSelection) {
22
+ document.getSelection = () => {
23
+ return options
24
+ }
25
+
26
+ document.execCommand = vi.fn()
27
+ }
28
+
29
+ const txt = 'test'
30
+
31
+ describe('copyToClipboard', () => {
32
+ it('copies text to the clipboard', () => {
33
+ mockDocument({
34
+ rangeCount: 0,
35
+ getRangeAt: () => null,
36
+ removeAllRanges: () => null,
37
+ })
38
+
39
+ const writeTextMock = vi.fn()
40
+
41
+ Object.defineProperty(navigator, 'clipboard', {
42
+ value: {
43
+ writeText: writeTextMock,
44
+ },
45
+ writable: true,
46
+ })
47
+
48
+ copyToClipboard(txt)
49
+
50
+ expect(writeTextMock).toHaveBeenCalledWith(txt)
51
+ expect(document.execCommand).not.toHaveBeenCalled()
52
+ })
53
+
54
+ it('copies text to the clipboard when text is already selected', () => {
55
+ mockDocument({
56
+ rangeCount: 2,
57
+ getRangeAt: (index: number) => ['a', 'b'][index],
58
+ removeAllRanges: () => null,
59
+ addRange: () => null,
60
+ })
61
+
62
+ const writeTextMock = vi.fn()
63
+
64
+ Object.defineProperty(navigator, 'clipboard', {
65
+ value: {
66
+ writeText: writeTextMock,
67
+ },
68
+ writable: true,
69
+ })
70
+
71
+ copyToClipboard(txt)
72
+
73
+ expect(writeTextMock).toHaveBeenCalledWith(txt)
74
+ expect(document.execCommand).not.toHaveBeenCalled()
75
+ })
76
+
77
+ it('copies text to the clipboard when text is already selected and navigator.clipboard is unavailable', () => {
78
+ mockDocument({
79
+ rangeCount: 2,
80
+ getRangeAt: (index: number) => ['a', 'b'][index],
81
+ removeAllRanges: () => null,
82
+ addRange: () => null,
83
+ })
84
+
85
+ Object.defineProperty(navigator, 'clipboard', {
86
+ value: null,
87
+ writable: true,
88
+ })
89
+
90
+ copyToClipboard(txt)
91
+
92
+ expect(document.execCommand).toHaveBeenCalledWith('copy')
93
+ })
94
+
95
+ it('does not copies text when getSelection is unavailable', () => {
96
+ document.getSelection = vi.fn(() => null)
97
+
98
+ expect(copyToClipboard(txt)).toBeUndefined()
99
+ })
100
+
101
+ afterEach(() => {
102
+ vi.clearAllMocks()
103
+ })
104
+ })
@@ -0,0 +1,37 @@
1
+ import type { BufferSource } from './types'
2
+
3
+ /** Download a file */
4
+ export function downloadFile(
5
+ content: BufferSource | Blob | string,
6
+ filename: string,
7
+ type: string,
8
+ utf8Bom = false, // UTF-8 header for Excel files
9
+ ): void {
10
+ /**
11
+ * \ufeff = UTF-8 encoding
12
+ * @see https://stackoverflow.com/a/18925211
13
+ */
14
+ const blobContent: BlobPart[] = utf8Bom ? ['\ufeff', content] : [content]
15
+ // https://stackoverflow.com/questions/69552023/after-update-typescript-3-7-2-to-latest-typescript-4-4-4-error-ts2339
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ const nav = window.navigator as any
18
+
19
+ const blob = new Blob(blobContent, { type })
20
+
21
+ if (nav.msSaveOrOpenBlob) {
22
+ nav.msSaveOrOpenBlob(blob, filename)
23
+ }
24
+ else {
25
+ const link = document.createElement('a')
26
+
27
+ link.target = '_blank'
28
+ link.style.display = 'none'
29
+ link.rel = 'noopener noreferrer'
30
+ link.href = window.URL.createObjectURL(blob)
31
+ link.download = filename
32
+ document.body.appendChild(link)
33
+ link.click()
34
+ document.body.removeChild(link)
35
+ window.URL.revokeObjectURL(link.href)
36
+ }
37
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
+
3
+ import { downloadFile } from '../'
4
+
5
+ describe('downloadFile', () => {
6
+ // https://stackoverflow.com/questions/69552023/after-update-typescript-3-7-2-to-latest-typescript-4-4-4-error-ts2339
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock IE navigator
8
+ const nav = window.navigator as any
9
+ const mockCreateObjectURL = vi.fn(() => 'mockObjectURL')
10
+ const mockRevokeObjectURL = vi.fn()
11
+
12
+ const file = new File(['test'], 'attestation.txt', {
13
+ type: 'text/plain',
14
+ })
15
+
16
+ beforeEach(() => {
17
+ global.URL.createObjectURL = mockCreateObjectURL
18
+ global.URL.revokeObjectURL = mockRevokeObjectURL
19
+ })
20
+ afterEach(() => {
21
+ vi.resetAllMocks()
22
+ document.body.innerHTML = ''
23
+ })
24
+
25
+ it('download a file', () => {
26
+ mockCreateObjectURL.mockReturnValue('http://exemple.com/')
27
+ downloadFile(file, file.name, file.type)
28
+ expect(mockCreateObjectURL).toHaveBeenCalledWith(
29
+ new Blob([file], { type: file.type }),
30
+ )
31
+ expect(mockRevokeObjectURL).toHaveBeenCalledWith('http://exemple.com/')
32
+ })
33
+
34
+ it('download a file with utf8Bom', () => {
35
+ mockCreateObjectURL.mockReturnValue('http://exemple.com/')
36
+ downloadFile(file, file.name, file.type, true)
37
+ expect(mockCreateObjectURL).toHaveBeenCalled()
38
+ expect(mockRevokeObjectURL).toHaveBeenCalledWith('http://exemple.com/')
39
+ })
40
+
41
+ it('download a file with content type BufferSource', () => {
42
+ const buffer = new ArrayBuffer(8)
43
+ mockCreateObjectURL.mockReturnValue('http://exemple.com/')
44
+ downloadFile(buffer, file.name, file.type)
45
+ expect(mockCreateObjectURL).toHaveBeenCalledWith(
46
+ new Blob([buffer], { type: file.type }),
47
+ )
48
+ expect(mockRevokeObjectURL).toHaveBeenCalledWith('http://exemple.com/')
49
+ })
50
+
51
+ it('download a file with content type String', () => {
52
+ mockCreateObjectURL.mockReturnValue('http://exemple.com/')
53
+ downloadFile('test', file.name, file.type)
54
+ expect(mockCreateObjectURL).toHaveBeenCalledWith(
55
+ new Blob(['test'], { type: file.type }),
56
+ )
57
+ expect(mockRevokeObjectURL).toHaveBeenCalledWith('http://exemple.com/')
58
+ })
59
+
60
+ it('download a file on Internet Explorer', () => {
61
+ const mockSaveOrOpenBlob = vi.fn()
62
+ nav.msSaveOrOpenBlob = mockSaveOrOpenBlob
63
+ downloadFile(file, file.name, file.type)
64
+ expect(mockSaveOrOpenBlob).toHaveBeenCalledWith(
65
+ new Blob([file], { type: file.type }),
66
+ file.name,
67
+ )
68
+ })
69
+ })
@@ -0,0 +1 @@
1
+ export type BufferSource = ArrayBufferView | ArrayBuffer