diff --git a/.eslintrc b/.eslintrc
index 4a700065..3ffc023e 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -2,7 +2,8 @@
"parser": "@babel/eslint-parser",
"env": {
"browser": true,
- "es6": true
+ "es6": true,
+ "jest/globals": true
},
"settings": {
"react": {
@@ -21,7 +22,8 @@
"plugins": [
"react",
"prefer-object-spread",
- "import"
+ "import",
+ "jest"
],
"extends": [
"umbrellio",
diff --git a/.github/workflows/ci-deploy.yml b/.github/workflows/ci-deploy.yml
index 0e0cc4d3..d3f5d9f7 100644
--- a/.github/workflows/ci-deploy.yml
+++ b/.github/workflows/ci-deploy.yml
@@ -12,9 +12,9 @@ jobs:
uses: actions/checkout@v2
- name: Set up Node.js
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
- node-version: '16.x'
+ node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0a9171b8..5575e726 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,6 +4,21 @@ on:
pull_request
jobs:
+ tests:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - run: yarn install --frozen-lockfile
+
+ - run: yarn build
+
+ - run: yarn test
storybook-build:
runs-on: ubuntu-latest
@@ -12,9 +27,9 @@ jobs:
uses: actions/checkout@v2
- name: Use Node.js
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
- node-version: '16'
+ node-version: '20'
- name: Run build
run: |
@@ -31,9 +46,9 @@ jobs:
uses: actions/checkout@v2
- name: Use Node.js
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
- node-version: '16'
+ node-version: '20'
- name: Run lints
run: |
@@ -49,9 +64,9 @@ jobs:
- uses: actions/checkout@v2
- name: Use Node.js
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
- node-version: '16'
+ node-version: '20'
- run: yarn install --frozen-lockfile
- run: yarn build
diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml
index c4665876..b60561a4 100644
--- a/.github/workflows/deploy-storybook.yml
+++ b/.github/workflows/deploy-storybook.yml
@@ -10,9 +10,9 @@ jobs:
uses: actions/checkout@v2
- name: Use Node.js
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
- node-version: '16.x'
+ node-version: '20.x'
- name: Build
run: |
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 00000000..0630face
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,11 @@
+/* eslint-env node */
+
+module.exports = {
+ bail: 1,
+ clearMocks: true,
+ moduleDirectories: ["node_modules"],
+ moduleFileExtensions: ["js"],
+ testMatch: [
+ "**/src/**/*.test.js",
+ ],
+}
diff --git a/package.json b/package.json
index 8f014b15..a42893d2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@cadolabs/sphere-ui",
- "version": "6.2.2",
+ "version": "6.2.3",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
@@ -21,7 +21,8 @@
"sb:lint": "bin/storybook lint",
"sb:eslint": "bin/storybook eslint",
"sb:stylelint": "bin/storybook stylelint",
- "prepublishOnly": "yarn build"
+ "prepublishOnly": "yarn build",
+ "test": "jest"
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
@@ -46,11 +47,13 @@
"eslint-config-umbrellio": "^5.0.1",
"eslint-formatter-table": "^7.32.1",
"eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-jest": "25.3.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.29.4",
+ "jest": "^29.7.0",
"postcss": "^8.4.31",
"postcss-scss": "^4.0.3",
"rollup": "^2.79.2",
diff --git a/src/components/EmojiPicker/asciiToEmoji/asciiToEmoji.test.js b/src/components/EmojiPicker/asciiToEmoji/asciiToEmoji.test.js
new file mode 100644
index 00000000..75dae783
--- /dev/null
+++ b/src/components/EmojiPicker/asciiToEmoji/asciiToEmoji.test.js
@@ -0,0 +1,27 @@
+import { asciiToEmoji } from "./index"
+
+const TEST_TEXT = "te=)st =)test test=) =)"
+
+describe("asciiToEmoji", () => {
+ it("should replace all emojis", () => {
+ expect(asciiToEmoji(TEST_TEXT))
+ .toBe("te๐st ๐test test๐ ๐")
+ })
+ it("should replace only emojis with spacing before", () => {
+ expect(asciiToEmoji(TEST_TEXT, { needSpacing: "before" }))
+ .toBe("te=)st ๐test test=) ๐")
+ })
+ it("should replace only emojis with spacing after", () => {
+ expect(asciiToEmoji(TEST_TEXT, { needSpacing: "after" }))
+ .toBe("te=)st =)test test๐ ๐")
+ })
+ it("should replace only emojis with spacing around", () => {
+ expect(asciiToEmoji(TEST_TEXT, { needSpacing: "around" }))
+ .toBe("te=)st =)test test=) ๐")
+ })
+ it("should throw error if inputString has incorrect type", () => {
+ expect(() => {
+ asciiToEmoji(91)
+ }).toThrow("inputString must be a string!")
+ })
+})
diff --git a/src/components/EmojiPicker/asciiToEmoji/index.js b/src/components/EmojiPicker/asciiToEmoji/index.js
new file mode 100644
index 00000000..36c4b76d
--- /dev/null
+++ b/src/components/EmojiPicker/asciiToEmoji/index.js
@@ -0,0 +1,168 @@
+import data from "@emoji-mart/data"
+
+const ASCII_TO_SHORTCODE_DICTIONARY = {
+ "<3": ":heart:",
+ "3": ":broken_heart:",
+ ":')": ":joy:",
+ ":'-)": ":joy:",
+ ":D": ":smiley:",
+ ":-D": ":smiley:",
+ "=D": ":smiley:",
+ ":)": ":slightly_smiling_face:",
+ ":-)": ":slightly_smiling_face:",
+ "=]": ":slightly_smiling_face:",
+ "=)": ":slightly_smiling_face:",
+ ":]": ":slightly_smiling_face:",
+ "':)": ":sweat_smile:",
+ "':-)": ":sweat_smile:",
+ "'=)": ":sweat_smile:",
+ "':D": ":sweat_smile:",
+ "':-D": ":sweat_smile:",
+ "'=D": ":sweat_smile:",
+ ">:)": ":laughing:",
+ ">;)": ":laughing:",
+ ">:-)": ":laughing:",
+ ">=)": ":laughing:",
+ ";)": ":wink:",
+ ";-)": ":wink:",
+ "*-)": ":wink:",
+ "*)": ":wink:",
+ ";-]": ":wink:",
+ ";]": ":wink:",
+ ";D": ":wink:",
+ ";^)": ":wink:",
+ "':(": ":sweat:",
+ "':-(": ":sweat:",
+ "'=(": ":sweat:",
+ ":*": ":kissing_heart:",
+ ":-*": ":kissing_heart:",
+ "=*": ":kissing_heart:",
+ ":^*": ":kissing_heart:",
+ ">:P": ":stuck_out_tongue_winking_eye:",
+ "X-P": ":stuck_out_tongue_winking_eye:",
+ "x-p": ":stuck_out_tongue_winking_eye:",
+ ">:[": ":disappointed:",
+ ":-(": ":disappointed:",
+ ":(": ":disappointed:",
+ ":-[": ":disappointed:",
+ ":[": ":disappointed:",
+ "=(": ":disappointed:",
+ ">:(:": ":angry:",
+ ">:-((": ":angry:",
+ ":@": ":angry:",
+ ":'(": ":cry:",
+ ":'-(": ":cry:",
+ ";(": ":cry:",
+ ";-(": ":cry:",
+ ">.<": ":persevere:",
+ "D:": ":fearful:",
+ ":$": ":flushed:",
+ "=$": ":flushed:",
+ "#-)": ":dizzy_face:",
+ "#)": ":dizzy_face:",
+ "%-)": ":dizzy_face:",
+ "%)": ":dizzy_face:",
+ "X)": ":dizzy_face:",
+ "X-)": ":dizzy_face:",
+ "*\\0/*": ":ok_woman:",
+ "\\0/": ":ok_woman:",
+ "*\\O/*": ":ok_woman:",
+ "\\O/": ":ok_woman:",
+ "O:-)": ":innocent:",
+ "0:-3": ":innocent:",
+ "0:3": ":innocent:",
+ "0:-)": ":innocent:",
+ "0:)": ":innocent:",
+ "0;^)": ":innocent:",
+ "O;-)": ":innocent:",
+ "O=)": ":innocent:",
+ "0;-)": ":innocent:",
+ "O:-3": ":innocent:",
+ "O:3": ":innocent:",
+ "B-)": ":sunglasses:",
+ "B)": ":sunglasses:",
+ "8)": ":sunglasses:",
+ "8-)": ":sunglasses:",
+ "B-D": ":sunglasses:",
+ "8-D": ":sunglasses:",
+ "-_-": ":expressionless:",
+ "-__-": ":expressionless:",
+ "-___-": ":expressionless:",
+ ">:\\": ":confused:",
+ ">:/": ":confused:",
+ ":-/": ":confused:",
+ ":-.": ":confused:",
+ ":/": ":confused:",
+ ":\\": ":confused:",
+ "=/": ":confused:",
+ "=\\": ":confused:",
+ ":L": ":confused:",
+ "=L": ":confused:",
+ ":P": ":stuck_out_tongue:",
+ ":-P": ":stuck_out_tongue:",
+ "=P": ":stuck_out_tongue:",
+ ":-p": ":stuck_out_tongue:",
+ ":p": ":stuck_out_tongue:",
+ "=p": ":stuck_out_tongue:",
+ ":-ร": ":stuck_out_tongue:",
+ ":ร": ":stuck_out_tongue:",
+ ":รพ": ":stuck_out_tongue:",
+ ":-รพ": ":stuck_out_tongue:",
+ ":-b": ":stuck_out_tongue:",
+ ":b": ":stuck_out_tongue:",
+}
+
+const ASCII_KEYS = Object.keys(ASCII_TO_SHORTCODE_DICTIONARY)
+
+const transformKeys = (keysArr, transformerFns = []) => {
+ let result = keysArr
+
+ transformerFns.forEach(fn => {
+ result = fn(result)
+ })
+
+ return result
+}
+
+const sortKeysByLength = arr => [...arr].sort((a, b) => b.length - a.length)
+const escapeSpecialSymbols = arr => arr.map(key => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
+const joinKeys = arr => arr.join("|")
+const asciiKeysRegex = transformKeys(ASCII_KEYS, [sortKeysByLength, escapeSpecialSymbols, joinKeys])
+
+const getByAscii = asciiKey => {
+ const id = ASCII_TO_SHORTCODE_DICTIONARY[asciiKey].replaceAll(":", "")
+
+ return data.emojis[id]?.skins[0]?.native
+}
+
+/**
+ * @param {string} inputString text that might contain ascii emoji
+ * @param {Object} [options]
+ * @param {"before" | "after" | "around"} [options.needSpacing]
+ * @returns {string} text with replacements
+ */
+export const asciiToEmoji = (inputString, options = {}) => {
+ if (typeof inputString !== "string") throw Error("inputString must be a string!")
+ const { needSpacing } = options
+
+ const needSpacingBeforeRegex = "(?<=\\s|^)"
+ const needSpacingAfterRegex = "(?=\\s|$)"
+
+ let condition = `${asciiKeysRegex}`
+
+ switch (needSpacing) {
+ case "before":
+ condition = `${needSpacingBeforeRegex}(${asciiKeysRegex})`
+ break
+ case "after":
+ condition = `(${asciiKeysRegex})${needSpacingAfterRegex}`
+ break
+ case "around":
+ condition = `${needSpacingBeforeRegex}(${asciiKeysRegex})${needSpacingAfterRegex}`
+ break
+ }
+
+ const regex = new RegExp(condition, "g")
+
+ return inputString.replace(regex, match => getByAscii(match))
+}
diff --git a/src/components/EmojiPicker/index.js b/src/components/EmojiPicker/index.js
index 819aed9c..5a2e18e4 100644
--- a/src/components/EmojiPicker/index.js
+++ b/src/components/EmojiPicker/index.js
@@ -1,7 +1,10 @@
import React from "react"
import { Picker } from "emoji-mart"
+
import data from "@emoji-mart/data"
+import { asciiToEmoji } from "./asciiToEmoji"
+
import translations from "./translations"
// because it has broken and unnecessary
@@ -40,3 +43,4 @@ const EmojiPicker = props => {
}
export default EmojiPicker
+export { asciiToEmoji }
diff --git a/storybook/i18n/locales/stories/emojiPicker/en.yml b/storybook/i18n/locales/stories/emojiPicker/en.yml
index b4003064..24cc888b 100644
--- a/storybook/i18n/locales/stories/emojiPicker/en.yml
+++ b/storybook/i18n/locales/stories/emojiPicker/en.yml
@@ -2,7 +2,10 @@ content:
main: Emoji is used to show emoji.
start:
title: Getting Started
- body: The EmojiPicker
component is based on emoji-mart Picker but is extended with additional translations.
+ component: The EmojiPicker
component is based on emoji-mart Picker but is extended with additional translations.
+ converter:
+ title: Additional utility
+ description: The asciiToEmoji
function replaces ASCII emojis with native browser emojis in the text.
props:
language: 'Available values: "en","bs","bg","hr","ro","sr","sv","de","fi","fr","ja","nl","pt","ru","tr","ar","be","cs","es","fa","hi","it","ko","pl","sa","uk","vi","zh",'
categories: 'Categories to show in the picker. Order is respected. Available values: "frequent", "people", "nature", "foods", "activity", "places", "objects", "symbols", "flags"'
diff --git a/storybook/stories/display/EmojiPicker/emojipicker.js b/storybook/stories/display/EmojiPicker/emojipicker.js
index fe06a017..95a3b787 100644
--- a/storybook/stories/display/EmojiPicker/emojipicker.js
+++ b/storybook/stories/display/EmojiPicker/emojipicker.js
@@ -1,15 +1,18 @@
/* eslint-disable max-len */
import i18n, { Trans } from "@i18n"
-import { Button, InputTextarea } from "@cadolabs/sphere-ui"
-import EmojiPicker from "@cadolabs/sphere-ui/EmojiPicker"
+import { Button, InputTextarea, InputSwitch, Dropdown } from "@cadolabs/sphere-ui"
+import { Highlighter } from "@components"
+import EmojiPicker, { asciiToEmoji } from "@cadolabs/sphere-ui/EmojiPicker"
import React from "react"
const I18N_PREFIX = "stories.emojiPicker"
const code = `
function Example () {
+ const textWithAscii = "te=)st =)test test=) =)"
const inputRef = React.useRef()
const [show, setShow] = React.useState(false)
+ const [isAsciiReplaceEnable, setAsciiReplaceEnable] = React.useState(false)
const [inputValue, setInputValue] = React.useState("")
const handleEmojiPick = event => {
@@ -32,35 +35,43 @@ function Example () {
}
return (
-
-
-
setInputValue(e.target.value)}
- className="w-30rem h-5rem"
- />
-
-