chore: add color validation + native color picker (#25355)

## Description
1. Reduced the number of default colors. Because the amount of suggested
options was too much: very little difference between shades and
sometimes with hues too. By removing half of them, we allow builders
make better choices faster. The transparent color has also been removed.
2. Added validation of color values because HTML colors are remarkably
easy to get wrong, because they allow so many different values and now
we support and validate all these guys
    - `hex` - `#bada55`
    - `name` - `LightGoldenrodYellow`
    - `special name` - `currentColor`
    - `rgb` - `rgb(0 0 0)`
    - `rgba` - `rgba(0, 0, 0, .45)`
    - `hsl` - `hsl(4.71239rad, 60%, 70%)`
    - `hsla` - `hsla(180deg 100% 50% / .8)`
    - `hwb` - `hwb(180deg 0% 0% / 100%)`
    - `lab` - `lab(2000.1337% -8.6911 -159.131231 / .987189732)`
    - `lch` - `lch(54.292% 106.839 40.853)`
<img width="283" alt="Снимок экрана 2023-08-02 в 17 58 07"
src="https://github.com/appsmithorg/appsmith/assets/11555074/a8fef365-506d-432e-85ad-cdb550de1f60">
    
3. Added support for a Full color picker. Now we can easily switch
between modes and builders can easily choose any colors.
<img width="259" alt="Снимок экрана 2023-08-02 в 17 43 34"
src="https://github.com/appsmithorg/appsmith/assets/11555074/be09cd92-7c69-43eb-812a-0b1fe3ac9ef6">


#### PR fixes following issue(s)
Fixes  #22996

#### Media

https://www.loom.com/share/098e0116e49744e7b10689d4a18ab664?sid=15405577-160e-4b48-bfef-bc8dcfa97efe

#### Type of change
- New feature (non-breaking change which adds functionality)

## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [x] Jest
- [x] Cypress

## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed

---------

Co-authored-by: Valera Melnikov <melnikov.vv@greendatasoft.ru>
This commit is contained in:
Valera Melnikov 2023-08-07 11:55:10 +03:00 committed by GitHub
parent 75eea5b87d
commit 8be4936ca0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 188 additions and 205 deletions

View File

@ -29,7 +29,7 @@ describe("Entity explorer Drag and Drop widgets testcases", function () {
cy.selectColor("backgroundcolor");
cy.get(formWidgetsPage.formD)
.should("have.css", "background-color")
.and("eq", "rgb(126, 34, 206)");
.and("eq", "rgb(219, 234, 254)");
/**
* @param{toggleButton Css} Assert to be checked
*/

View File

@ -174,7 +174,7 @@ describe("Undo/Redo functionality", function () {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.wait("@updateLayout");
cy.readTextDataValidateCSS("color", "rgb(126, 34, 206)");
cy.readTextDataValidateCSS("color", "rgb(219, 234, 254)");
cy.get("body").click({ force: true }).type(`{${modifierKey}}z`);
entityExplorer.NavigateToSwitcher("Explorer");
entityExplorer.SelectEntityByName("Text1");
@ -191,7 +191,7 @@ describe("Undo/Redo functionality", function () {
cy.get(widgetsPage.textColor)
.first()
.invoke("attr", "value")
.should("contain", "#7e22ce");
.should("contain", "#dbeafe");
});
it("8. checks undo/redo for option control for radio button", function () {

View File

@ -364,7 +364,7 @@ describe("App Theming funtionality", function () {
.find(".t--theme-card > main > main")
.invoke("css", "background-color")
.then((backgroudColor) => {
expect(backgroudColor).to.eq("rgb(131, 24, 67)");
expect(backgroudColor).to.eq("rgb(236, 72, 153)");
});
//Check if the saved theme is present under 'Yours Themes' section with Trash button
@ -669,7 +669,7 @@ describe("App Theming funtionality", function () {
.eq(0)
.invoke("css", "background-color")
.then((backgroudColor) => {
expect(backgroudColor).to.eq("rgb(126, 34, 206)");
expect(backgroudColor).to.eq("rgb(219, 234, 254)");
});
cy.contains("Applied theme")
@ -680,7 +680,7 @@ describe("App Theming funtionality", function () {
.eq(1)
.invoke("css", "background-color")
.then((backgroudColor) => {
expect(backgroudColor).to.eq("rgb(253, 224, 71)");
expect(backgroudColor).to.eq("rgb(29, 78, 216)");
});
//#endregion
@ -696,17 +696,17 @@ describe("App Theming funtionality", function () {
cy.xpath("//div[@id='root']//section/parent::div").should(
"have.css",
"background-color",
"rgb(253, 224, 71)",
"rgb(29, 78, 216)",
); //Background Color
cy.get(widgetsPage.widgetBtn).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
); //Widget Color
cy.get(publish.iconWidgetBtn).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
); //Widget Color
cy.get(widgetsPage.widgetBtn).should("have.css", "border-radius", "24px"); //Border Radius
@ -738,12 +738,12 @@ describe("App Theming funtionality", function () {
cy.get(".t--widget-button1 button").should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
); //old widgets still conforming to theme color
cy.get(widgetsPage.iconWidgetBtn).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
});
@ -789,20 +789,20 @@ describe("App Theming funtionality", function () {
cy.get(".t--widget-buttonwidget:nth-child(4) button").should(
"have.css",
"background-color",
"rgb(134, 239, 172)", //rgb(134, 239, 172)
"rgb(190, 24, 93)",
); //new widget with its own color
////old widgets still conforming to theme color
cy.get(".t--widget-buttonwidget button").should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
cy.get(publish.iconWidgetBtn).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
//Verify Border radius
@ -845,7 +845,7 @@ describe("App Theming funtionality", function () {
cy.get(".t--widget-button2 button").should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
); //verify widget reverted to theme color
cy.get(".t--property-control-borderradius .reset-button").then(($elem) => {
$elem[0].removeAttribute("display: none");
@ -866,12 +866,12 @@ describe("App Theming funtionality", function () {
cy.xpath("//div[@id='root']//section/parent::div").should(
"have.css",
"background-color",
"rgb(253, 224, 71)",
"rgb(29, 78, 216)",
); //Background Color
cy.get(".t--widget-button1 button").should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
); //Widget Color
cy.get("body").then(($ele) => {
if ($ele.find(widgetsPage.widgetBtn).length <= 1) {
@ -882,12 +882,12 @@ describe("App Theming funtionality", function () {
cy.get(".t--widget-button2 button").should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
); //Widget Color
cy.get(publish.iconWidgetBtn).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
); //Widget Color
cy.get(".t--widget-button1 button").should(
@ -1004,7 +1004,7 @@ describe("App Theming funtionality", function () {
cy.get(".t--widget-button1 button").should(
"have.css",
"background-color",
"rgb(252, 165, 165)",
"rgb(161, 98, 7)",
); //new widget with its own color
////old widgets still conforming to theme color

View File

@ -93,7 +93,7 @@ describe("Theme validation usecases", function () {
cy.get(themelocator.inputColor).clear({ force: true });
cy.wait(2000);
theme.ChangeThemeColor(16, "Background");
cy.get(themelocator.inputColor).should("have.value", "#dc2626"); //Red
cy.get(themelocator.inputColor).should("have.value", "#86efac"); //Red
cy.wait(2000);
cy.get(themelocator.inputColor).eq(0).click({ force: true });
@ -107,7 +107,7 @@ describe("Theme validation usecases", function () {
cy.get(themelocator.inputColor).clear({ force: true });
cy.wait(2000);
theme.ChangeThemeColor(9, "Primary");
cy.get(themelocator.inputColor).should("have.value", "#18181b"); //Black
cy.get(themelocator.inputColor).should("have.value", "#7f1d1d"); //Black
cy.wait(2000);
cy.contains("Color").click({ force: true });
appSettings.ClosePane();

View File

@ -190,13 +190,13 @@ describe("List Widget Functionality", function () {
agHelper.AssertCSS(
locators._listWidget,
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
// Verify List Item Background Color
agHelper.AssertCSS(
locators._itemContainerWidget,
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
deployMode.NavigateBacktoEditor();
});

View File

@ -37,13 +37,13 @@ describe("Container Widget Functionality", function () {
cy.get(widgetsPage.listWidget).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
// Verify List Item Background Color
cy.get(widgetsPage.itemContainerWidget).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
_.deployMode.NavigateBacktoEditor();
});

View File

@ -16,7 +16,7 @@ describe("Table Widget property pane feature validation", function () {
cy.wait(500);
cy.wait("@updateLayout");
// Verify the text color is green
cy.readTabledataValidateCSS("1", "0", "color", "rgb(126, 34, 206)");
cy.readTabledataValidateCSS("1", "0", "color", "rgb(219, 234, 254)");
// Change the text color and enter purple in input field
cy.get(widgetsPage.textColor)
.scrollIntoView()
@ -41,13 +41,15 @@ describe("Table Widget property pane feature validation", function () {
"1",
"1",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
_.deployMode.NavigateBacktoEditor();
cy.openPropertyPane("tablewidget");
// Change the cell background color and enter purple in input field
cy.get(`${widgetsPage.cellBackground_tablev1} input`)
cy.get(
`${widgetsPage.cellBackground_tablev1} [data-testid='t--color-picker-input']`,
)
.clear({ force: true })
.type("purple", { force: true });
cy.wait("@updateLayout");

View File

@ -28,23 +28,23 @@ describe("Table Widget empty row color validation", function () {
"1",
"0",
"background-color",
"rgb(99, 102, 241)",
"rgb(185, 28, 28)",
);
// Verify the cell background color of second column
cy.readTabledataValidateCSS(
"1",
"1",
"background-color",
"rgb(30, 58, 138)",
"rgb(113, 113, 122)",
);
//Test 2. Validate empty row background
// first cell of first row should be transparent
cy.get(
".t--widget-tablewidget .tbody div[data-testid='empty-row-0-cell-0']",
).should("have.css", "background-color", "rgb(99, 102, 241)");
).should("have.css", "background-color", "rgb(185, 28, 28)");
// second cell of first row should be transparent
cy.get(
".t--widget-tablewidget .tbody div[data-testid='empty-row-0-cell-1']",
).should("have.css", "background-color", "rgb(30, 58, 138)");
).should("have.css", "background-color", "rgb(113, 113, 122)");
});
});

View File

@ -171,12 +171,12 @@ describe("Table Widget property pane feature validation", function () {
it("6. Test to validate text color and text background", function () {
cy.openPropertyPane("tablewidget");
// Changing text color to rgb(126, 34, 206) and validate
// Changing text color to rgb(219, 234, 254) and validate
cy.selectColor("textcolor");
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5000);
cy.wait("@updateLayout");
cy.readTabledataValidateCSS("1", "0", "color", "rgb(126, 34, 206)");
cy.readTabledataValidateCSS("1", "0", "color", "rgb(219, 234, 254)");
// Changing text color to PURPLE and validate using JS
cy.get(widgetsPage.toggleJsColor).click({ force: true });
@ -185,13 +185,13 @@ describe("Table Widget property pane feature validation", function () {
cy.wait("@updateLayout");
cy.readTabledataValidateCSS("1", "0", "color", "rgb(128, 0, 128)");
cy.get(commonlocators.editPropBackButton).click();
// Changing Cell backgroud color to rgb(126, 34, 206) and validate
// Changing Cell backgroud color to rgb(219, 234, 254) and validate
cy.selectColor("cellbackgroundcolor");
cy.readTabledataValidateCSS(
"0",
"0",
"background",
"rgb(126, 34, 206) none repeat scroll 0% 0% / auto padding-box border-box",
"rgb(219, 234, 254) none repeat scroll 0% 0% / auto padding-box border-box",
true,
);
// Changing Cell backgroud color to PURPLE and validate using JS

View File

@ -325,20 +325,4 @@ describe("Table Widget property pane feature validation", function () {
cy.wait(500);
cy.get("[data-testid='t--property-pane-back-btn']").click({ force: true });
});
it("7. Table widget test on button when transparent", () => {
cy.openPropertyPane("tablewidget");
// Open column details of "id".
cy.editColumn("id");
// Changing column "Button" color to transparent
cy.get(widgetsPage.buttonColor).click({ force: true });
cy.wait(2000);
cy.get(widgetsPage.transparent).click({ force: true });
cy.get(".td[data-colindex=5][data-rowindex=0] .bp3-button").should(
"have.css",
"background-color",
"rgba(0, 0, 0, 0)",
);
});
});

View File

@ -24,7 +24,7 @@ describe("Table Widget V2 property pane feature validation", function () {
cy.wait(500);
cy.wait("@updateLayout");
// Verify the text color is green
cy.readTableV2dataValidateCSS("1", "0", "color", "rgb(126, 34, 206)");
cy.readTableV2dataValidateCSS("1", "0", "color", "rgb(219, 234, 254)");
// Change the text color and enter purple in input field
cy.get(widgetsPage.textColor)
.scrollIntoView()
@ -49,13 +49,15 @@ describe("Table Widget V2 property pane feature validation", function () {
"1",
"1",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
_.deployMode.NavigateBacktoEditor();
cy.openPropertyPane("tablewidgetv2");
cy.moveToStyleTab();
// Change the cell background color and enter purple in input field
cy.get(`.t--property-control-cellbackgroundcolor input`)
cy.get(
`.t--property-control-cellbackgroundcolor [data-testid='t--color-picker-input']`,
)
.clear({ force: true })
.type("purple", { force: true });
cy.wait("@updateLayout");

View File

@ -18,12 +18,12 @@ describe("Table Widget V2 property pane feature validation", function () {
cy.openPropertyPane("tablewidgetv2");
cy.editColumn("id");
cy.moveToStyleTab();
// Changing text color to rgb(126, 34, 206) and validate
// Changing text color to rgb(219, 234, 254) and validate
cy.selectColor("textcolor");
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5000);
cy.wait("@updateLayout");
cy.readTableV2dataValidateCSS("1", "0", "color", "rgb(126, 34, 206)");
cy.readTableV2dataValidateCSS("1", "0", "color", "rgb(219, 234, 254)");
// Changing text color to PURPLE and validate using JS
cy.get(widgetsPage.toggleJsColor).click();
@ -31,13 +31,13 @@ describe("Table Widget V2 property pane feature validation", function () {
cy.wait("@updateLayout");
cy.readTableV2dataValidateCSS("1", "0", "color", "rgb(128, 0, 128)");
// Changing Cell backgroud color to rgb(126, 34, 206) and validate
// Changing Cell backgroud color to rgb(219, 234, 254) and validate
cy.selectColor("cellbackground");
cy.readTableV2dataValidateCSS(
"0",
"0",
"background",
"rgb(113, 30, 184) none repeat scroll 0% 0% / auto padding-box border-box",
"rgb(194, 220, 253) none repeat scroll 0% 0% / auto padding-box border-box",
true,
);
// Changing Cell backgroud color to PURPLE and validate using JS

View File

@ -289,20 +289,4 @@ describe("Table Widget V2 property pane feature validation", function () {
cy.wait(500);
cy.get("[data-testid='t--property-pane-back-btn']").click({ force: true });
});
it("8. Table widget test on button when transparent", () => {
cy.openPropertyPane("tablewidgetv2");
// Open column details of "id".
cy.editColumn("id");
// Changing column "Button" color to transparent
cy.moveToStyleTab();
cy.get(widgetsPage.buttonColor).click({ force: true });
cy.wait(2000);
cy.get(widgetsPage.transparent).click({ force: true });
cy.get(".td[data-colindex=5][data-rowindex=0] .bp3-button").should(
"have.css",
"background-color",
"rgba(0, 0, 0, 0)",
);
});
});

View File

@ -21,7 +21,7 @@ describe("Text Widget Cell Background and Text Size Validation", function () {
cy.get(`${widgetsPage.textWidget} .bp3-ui-text`).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
//Toggle to JS mode

View File

@ -85,7 +85,7 @@ describe("Text Widget color/font/alignment Functionality", function () {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.wait("@updateLayout");
cy.readTextDataValidateCSS("color", "rgb(126, 34, 206)");
cy.readTextDataValidateCSS("color", "rgb(219, 234, 254)");
cy.get(widgetsPage.textColor)
.clear({ force: true })
.type("purple", { force: true });
@ -105,7 +105,7 @@ describe("Text Widget color/font/alignment Functionality", function () {
cy.get(`${widgetsPage.textWidget} .bp3-ui-text`).should(
"have.css",
"background-color",
"rgb(126, 34, 206)",
"rgb(219, 234, 254)",
);
//Toggle JS check with cell background:

View File

@ -21,7 +21,7 @@ describe("Json & JsonB Datatype tests", function () {
agHelper.AddDsl("Datatypes/JsonDTdsl");
entityExplorer.NavigateToSwitcher("Widgets");
appSettings.OpenPaneAndChangeThemeColors(33, 39);
appSettings.OpenPaneAndChangeThemeColors(16, 20);
});
beforeEach(() => {

View File

@ -4,7 +4,7 @@
"popover": ".rc-tooltip-inner",
"shadow": ".t--theme-appBoxShadow",
"color": ".t--property-pane-sidebar .bp3-popover-target .cursor-pointer",
"inputColor": ".t--colorpicker-v2-popover input",
"inputColor": ".t--colorpicker-v2-popover [data-testid='t--color-picker-input']",
"colorPicker": "[data-testid='color-picker']",
"greenColor": "[style='background-color: rgb(21, 128, 61);']",
"fontsSelected": ".leading-normal",

View File

@ -8,7 +8,7 @@
"inputPropsDataType": ".t--property-control-datatype input",
"inputdatatypeplaceholder": ".t--property-control-placeholder",
"buttonWidget": ".t--draggable-buttonwidget",
"buttonColor": ".t--property-control-buttoncolor input",
"buttonColor": ".t--property-control-buttoncolor [data-testid='t--color-picker-input']",
"checkboxWidget": ".t--draggable-checkboxwidget",
"buttonStyleDropdown": ".t--property-control-buttonstyle [name='downArrow']",
"buttonBackground": ".sc-ecQjpJ > div > .bp3-button",
@ -38,7 +38,7 @@
"requiredjs": ".t--property-control-required input",
"visible": ".t--property-control-visible input",
"disable": ".t--property-control-disabled",
"menuColor": ".t--property-control-menucolor input",
"menuColor": ".t--property-control-menucolor [data-testid='t--color-picker-input']",
"menubar": ".bp3-menu",
"menupop": ".bp3-popover",
"defaultcheck": ".t--property-control-defaultstate input",
@ -94,17 +94,16 @@
"verticalTop": "[data-value='TOP']",
"verticalCenter": "[data-value='CENTER']",
"verticalBottom": "[data-value='BOTTOM']",
"textColor": ".t--property-control-textcolor input",
"textColor": ".t--property-control-textcolor [data-testid='t--color-picker-input']",
"boadercolorPicker": ".t--property-control-bordercolour input",
"boxShadowColorPicker": ".t--property-control-shadowcolor input",
"boxShadow": ".t--property-control-boxshadow .bp3-button-group",
"inputStepArrows": ".bp3-button-group",
"backgroundcolorPicker": ".t--property-control-backgroundcolour input",
"backgroundcolorPickerNew": ".t--property-control-backgroundcolor input",
"backgroundcolorPickerNew": ".t--property-control-backgroundcolor [data-testid='t--color-picker-input']",
"greenColorHex": "#03b365",
"yellowColorHex": "#FFC13D",
"greenColor": "//div[@color='#03b365']",
"transparent": ".diagnol-cross",
"yellowColor": "//div[@color='#FFC13D']",
"blueColor": "//div[@color='#3366FF']",
"toggleJsColor": ".t--property-control-textcolor .t--js-toggle",

View File

@ -10,7 +10,7 @@ export class ThemeSettings {
"']//ancestor::div[@class= 'space-y-1 group']",
_colorPickerV2Popover: ".t--colorpicker-v2-popover",
_colorPickerV2Color:
"//h3[text()='All Colors']/following-sibling::div//div[contains(@class,'t--colorpicker-v2-color')]",
"[data-testid='t--all-colors'] .t--colorpicker-v2-color",
_colorRingPrimary: "[data-testid='theme-primaryColor']",
_colorRingBackground: "[data-testid='theme-backgroundColor']",
_colorInput: (option: string) =>

View File

@ -207,6 +207,7 @@
"unescape-js": "^1.1.4",
"url-search-params-polyfill": "^8.0.0",
"uuid": "^9.0.0",
"validate-color": "^2.2.4",
"webfontloader": "^1.6.28",
"webpack-retry-chunk-load-plugin": "^3.1.1",
"yjs": "^13.5.12",

View File

@ -56,13 +56,6 @@ and .Toastify__toast-container--bottom-center classes, which messes with the pla
opacity: 0.5;
}
.diagnol-cross {
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' preserveAspectRatio='none' viewBox='0 0 100 100'><path d='M0 99 L99 0 L100 1 L1 100' fill='red' /></svg>");
background-repeat: no-repeat;
background-position: center center;
background-size: 100% 100%, auto;
}
.hidden-scrollbar {
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* for Firefox */

View File

@ -1901,3 +1901,6 @@ export const DEFAULT_CAMERA_LABEL_DESCRIPTION = () =>
"Default choice for mobile users. Not applicable for other devices";
export const FRONT_CAMERA_LABEL = () => "Front (Selfie)";
export const BACK_CAMERA_LABEL = () => "Back (Rear)";
// Color picker
export const FULL_COLOR_PICKER_LABEL = () => "Full color picker";

View File

@ -111,6 +111,7 @@ describe("<ColorPicker /> - Keyboard Navigation", () => {
userEvent.tab();
userEvent.keyboard("{Enter}");
userEvent.tab();
userEvent.tab();
expect(
document.querySelectorAll("[tabindex='0'].t--colorpicker-v2-color")[0],
@ -133,6 +134,7 @@ describe("<ColorPicker /> - Keyboard Navigation", () => {
userEvent.tab();
userEvent.keyboard("{Enter}");
userEvent.tab();
userEvent.tab();
userEvent.tab();
@ -153,6 +155,7 @@ describe("<ColorPicker /> - Keyboard Navigation", () => {
userEvent.tab();
userEvent.keyboard("{Enter}");
userEvent.tab();
userEvent.tab();
userEvent.tab();
@ -180,6 +183,7 @@ describe("<ColorPicker /> - Keyboard Navigation", () => {
userEvent.tab();
userEvent.keyboard("{Enter}");
userEvent.tab();
userEvent.tab();
userEvent.tab();
@ -199,6 +203,7 @@ describe("<ColorPicker /> - Keyboard Navigation", () => {
userEvent.tab();
userEvent.keyboard("{Enter}");
userEvent.tab();
userEvent.tab();
userEvent.tab();

View File

@ -6,7 +6,7 @@ import React, {
useCallback,
} from "react";
import styled from "styled-components";
import { Icon } from "design-system";
import { Icon, Switch } from "design-system";
import {
Popover,
InputGroup,
@ -21,11 +21,15 @@ import {
getThemePropertyBinding,
} from "constants/ThemeConstants";
import { getWidgets } from "sagas/selectors";
import { extractColorsFromString } from "utils/helpers";
import { extractColorsFromString, isValidColor } from "utils/helpers";
import { TAILWIND_COLORS } from "constants/ThemeConstants";
import useDSEvent from "utils/hooks/useDSEvent";
import { DSEventTypes } from "utils/AppsmithUtils";
import { getBrandColors } from "@appsmith/selectors/tenantSelectors";
import {
createMessage,
FULL_COLOR_PICKER_LABEL,
} from "@appsmith/constants/messages";
const FocusTrap = require("focus-trap-react");
@ -46,6 +50,7 @@ interface ColorPickerProps {
isOpen?: boolean;
placeholderText?: string;
portalContainer?: HTMLElement;
onPopupClosed?: () => void;
}
/**
@ -73,7 +78,10 @@ const ColorPickerIconContainer = styled.div`
z-index: 1;
`;
const StyledInputGroup = styled(InputGroup)`
const StyledInputGroup = styled(InputGroup)<{
$isValid?: boolean;
$isFullColorPicker?: boolean;
}>`
.${Classes.INPUT} {
box-shadow: none;
border: 1px solid var(--ads-v2-color-border);
@ -83,23 +91,30 @@ const StyledInputGroup = styled(InputGroup)`
}
}
&&& input {
padding-left: 36px;
padding: ${({ $isFullColorPicker }) =>
$isFullColorPicker ? "0px 2px" : "0 10px 0 36px"};
height: 36px;
border: 1px solid var(--ads-v2-color-border);
border: ${({ $isValid }) =>
$isValid
? "1px solid var(--ads-v2-color-border)"
: "1px solid var(--ads-v2-color-border-error)"};
background: ${(props) =>
props.theme.colors.propertyPane.multiDropdownBoxHoverBg};
color: ${(props) => props.theme.colors.propertyPane.label};
&:hover {
border-color: var(--ads-v2-color-border-emphasis);
}
&:focus {
border: 1px solid var(--ads-v2-color-border-emphasis);
outline: var(--ads-v2-border-width-outline) solid
var(--ads-v2-color-outline);
outline-offset: var(--ads-v2-offset-outline);
}
&:hover,
&:focus {
border-color: ${({ $isValid }) =>
$isValid
? "var(--ads-v2-color-border-emphasis)"
: "var(--ads-v2-color-border-error)"};
}
}
`;
@ -117,7 +132,6 @@ interface ColorPickerPopupProps {
const PopupContainer = styled.div`
padding: 0.75rem;
width: 18rem;
border-radius: var(--ads-v2-border-radius);
border: 1px solid var(--ads-v2-color-border);
`;
@ -126,10 +140,9 @@ function ColorPickerPopup(props: ColorPickerPopupProps) {
const themeColors = useSelector(getSelectedAppThemeProperties).colors;
const brandColors = useSelector(getBrandColors);
const widgets = useSelector(getWidgets);
const DSLStringified = JSON.stringify(widgets);
const applicationColors = useMemo(() => {
return extractColorsFromString(DSLStringified);
}, [DSLStringified]);
return extractColorsFromString(widgets);
}, []);
const {
changeColor,
color,
@ -169,7 +182,7 @@ function ColorPickerPopup(props: ColorPickerPopupProps) {
<h2 className="pb-2 font-semibold border-b">Color Styles</h2>
<section className="space-y-2">
<h3 className="text-xs">Theme Colors</h3>
<div className="grid grid-cols-10 gap-2">
<div className="grid grid-cols-5 gap-2">
{Object.keys(themeColors).map((colorKey, colorIndex) => (
<div
className={`${COLOR_BOX_CLASSES} ${
@ -199,7 +212,7 @@ function ColorPickerPopup(props: ColorPickerPopupProps) {
{brandColors && Object.keys(brandColors).length > 0 && (
<section className="space-y-2">
<h3 className="text-xs">Brand Colors</h3>
<div className="grid grid-cols-10 gap-2">
<div className="grid grid-cols-5 gap-2">
{Object.keys(brandColors).map(
(colorKey: string, colorIndex: number) => (
<div
@ -223,7 +236,7 @@ function ColorPickerPopup(props: ColorPickerPopupProps) {
{showApplicationColors && applicationColors.length > 0 && (
<section className="space-y-2">
<h3 className="text-xs">Application Colors</h3>
<div className="grid grid-cols-10 gap-2">
<div className="grid grid-cols-5 gap-2">
{Object.values(applicationColors).map(
(colorCode: string, colorIndex) => (
<div
@ -246,8 +259,15 @@ function ColorPickerPopup(props: ColorPickerPopupProps) {
)}
<section className="space-y-2">
<h3 className="text-xs">All Colors</h3>
<div className="grid grid-cols-10 gap-2 t--tailwind-colors">
{(showThemeColors ||
(brandColors && Object.keys(brandColors).length > 0) ||
(showApplicationColors && applicationColors.length > 0)) && (
<h3 className="text-xs">All Colors</h3>
)}
<div
className="grid grid-cols-5 gap-2 t--tailwind-colors"
data-testid="t--all-colors"
>
{Object.keys(TAILWIND_COLORS).map((colorKey, rowIndex) =>
Object.keys(get(TAILWIND_COLORS, `${colorKey}`)).map(
(singleColorKey, colIndex) => (
@ -275,27 +295,6 @@ function ColorPickerPopup(props: ColorPickerPopupProps) {
),
),
)}
<div
className={`${COLOR_BOX_CLASSES} ${
color === "#fff" ? "ring-1" : ""
}`}
onClick={(e) => {
setColor("#fff");
changeColor("#fff", !e.isTrusted);
}}
tabIndex={-1}
/>
<div
className={`${COLOR_BOX_CLASSES} diagnol-cross ${
color === "transparent" ? "ring-1" : ""
}`}
onClick={(e) => {
setColor("transparent");
changeColor("transparent", !e.isTrusted);
}}
tabIndex={-1}
/>
</div>
</section>
</PopupContainer>
@ -329,7 +328,7 @@ interface LeftIconProps {
}
function LeftIcon(props: LeftIconProps) {
return props.color ? (
return isValidColor(props.color) ? (
<ColorIcon
className="rounded-full cursor-pointer"
color={props.color}
@ -365,6 +364,8 @@ const ColorPickerComponent = React.forwardRef(
props.evaluatedColorValue || props.color,
);
const [isFullColorPicker, setFullColorPicker] = React.useState(false);
const debouncedOnChange = React.useCallback(
debounce((color: string, isUpdatedViaKeyboard: boolean) => {
props.changeColor(color, isUpdatedViaKeyboard);
@ -417,7 +418,6 @@ const ColorPickerComponent = React.forwardRef(
}
break;
case "Enter":
case " ":
emitKeyPressEvent(e.key);
(document.activeElement as any)?.click();
setTimeout(() => {
@ -530,7 +530,9 @@ const ColorPickerComponent = React.forwardRef(
const handleChangeColor = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
debouncedOnChange(value, true);
if (isValidColor(value)) {
debouncedOnChange(value, true);
}
setColor(value);
};
@ -544,9 +546,20 @@ const ColorPickerComponent = React.forwardRef(
const handleInputClick = () => {
isClick.current = true;
if (isFullColorPicker && isOpen) {
setIsOpen(false);
}
};
const handleFullColorPickerClick = (value: boolean) => {
setFullColorPicker(value);
setIsOpen(false);
};
const handleOnInteraction = (nextOpenState: boolean) => {
if (isFullColorPicker && !isOpen) return;
if (isOpen !== nextOpenState) {
if (isClick.current) setIsOpen(true);
else setIsOpen(nextOpenState);
@ -567,19 +580,26 @@ const ColorPickerComponent = React.forwardRef(
isOpen={isOpen}
minimal
modifiers={POPOVER_MODFIER}
onClosed={props.onPopupClosed}
onInteraction={handleOnInteraction}
popoverClassName="color-picker-input"
portalContainer={props.portalContainer}
>
<StyledInputGroup
$isFullColorPicker={isFullColorPicker}
$isValid={isValidColor(color)}
autoFocus={props.autoFocus}
data-testid="t--color-picker-input"
inputRef={inputGroupRef}
leftIcon={
<LeftIcon color={color} handleInputClick={handleInputClick} />
!isFullColorPicker ? (
<LeftIcon color={color} handleInputClick={handleInputClick} />
) : null
}
onChange={handleChangeColor}
onClick={handleInputClick}
placeholder={placeholderText || "enter color name or hex"}
type={isFullColorPicker ? "color" : "text"}
value={color}
/>
@ -593,6 +613,14 @@ const ColorPickerComponent = React.forwardRef(
showThemeColors={props.showThemeColors}
/>
</Popover>
<div className="mt-2">
<Switch
isSelected={isFullColorPicker}
onChange={handleFullColorPickerClick}
>
{createMessage(FULL_COLOR_PICKER_LABEL)}
</Switch>
</div>
</div>
);
},

View File

@ -13,101 +13,54 @@ export type TailwindColors = {
export const TAILWIND_COLORS: TailwindColors = {
gray: {
50: "#fafafa",
100: "#f4f4f5",
200: "#e4e4e7",
300: "#d4d4d8",
400: "#a1a1aa",
500: "#71717a",
600: "#52525b",
700: "#3f3f46",
800: "#27272a",
900: "#18181b",
},
red: {
50: "#fef2f2",
100: "#fee2e2",
200: "#fecaca",
300: "#fca5a5",
400: "#f87171",
500: "#ef4444",
600: "#dc2626",
700: "#b91c1c",
800: "#991b1b",
900: "#7f1d1d",
},
yellow: {
50: "#fefce8",
100: "#fef9c3",
200: "#fef08a",
300: "#fde047",
400: "#facc15",
500: "#eab308",
600: "#ca8a04",
700: "#a16207",
800: "#854d0e",
900: "#713f12",
},
green: {
50: "#f0fdf4",
100: "#dcfce7",
200: "#bbf7d0",
300: "#86efac",
400: "#4ade80",
500: "#22c55e",
600: "#16a34a",
700: "#15803d",
800: "#166534",
900: "#14532d",
},
blue: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
},
indigo: {
50: "#eef2ff",
100: "#e0e7ff",
200: "#c7d2fe",
300: "#a5b4fc",
400: "#818cf8",
500: "#6366f1",
600: "#4f46e5",
700: "#4338ca",
800: "#3730a3",
900: "#312e81",
},
purple: {
50: "#faf5ff",
100: "#f3e8ff",
200: "#e9d5ff",
300: "#d8b4fe",
400: "#c084fc",
500: "#a855f7",
600: "#9333ea",
700: "#7e22ce",
800: "#6b21a8",
900: "#581c87",
},
pink: {
50: "#fdf2f8",
100: "#fce7f3",
200: "#fbcfe8",
300: "#f9a8d4",
400: "#f472b6",
500: "#ec4899",
600: "#db2777",
700: "#be185d",
800: "#9d174d",
900: "#831843",
},
};

View File

@ -80,6 +80,7 @@ function ThemeColorControl(props: ThemeColorControlProps) {
color={userDefinedColors[selectedColor]}
isOpen={autoFocus}
key={selectedColor}
onPopupClosed={() => setAutoFocus(false)}
portalContainer={
document.getElementById("app-settings-portal") || undefined
}

View File

@ -1,6 +1,7 @@
import { RenderModes } from "constants/WidgetConstants";
import { ValidationTypes } from "constants/WidgetValidation";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import type { CanvasWidgetsReduxState } from "../reducers/entityReducers/canvasWidgetsReducer";
import { AutocompleteDataType } from "./autocomplete/AutocompleteDataType";
import {
flattenObject,
@ -550,20 +551,30 @@ describe("#captureInvalidDynamicBindingPath", () => {
describe("#extractColorsFromString", () => {
it("Check if the extractColorsFromString returns rgb, rgb, hex color strings", () => {
const borderWithHex = `2px solid ${Colors.GREEN}`;
const borderWithRgb = "2px solid rgb(0,0,0)";
const borderWithRgba = `2px solid ${Colors.BOX_SHADOW_DEFAULT_VARIANT1}`;
const widgets = {
0: { color: `${Colors.GREEN}` },
1: { color: "rgb(0,0,0)" },
2: { color: `${Colors.BOX_SHADOW_DEFAULT_VARIANT1}` },
3: { color: `LightGoldenrodYellow` },
4: { color: `lch(54.292% 106.839 40.853)` },
} as unknown as CanvasWidgetsReduxState;
//Check Hex value
expect(extractColorsFromString(borderWithHex)[0]).toEqual("#03b365");
//Check rgba value
expect(extractColorsFromString(borderWithRgba)[0]).toEqual(
"rgba(0, 0, 0, 0.25)",
);
expect(extractColorsFromString(widgets)[0]).toEqual("#03B365");
//Check rgb
expect(extractColorsFromString(borderWithRgb)[0]).toEqual("rgb(0,0,0)");
expect(extractColorsFromString(widgets)[1]).toEqual("rgb(0,0,0)");
//Check rgba value
expect(extractColorsFromString(widgets)[2]).toEqual("rgba(0, 0, 0, 0.25)");
//Check name value
expect(extractColorsFromString(widgets)[3]).toEqual("LightGoldenrodYellow");
//Check lch value
expect(extractColorsFromString(widgets)[4]).toEqual(
"lch(54.292% 106.839 40.853)",
);
});
});

View File

@ -35,6 +35,7 @@ import type { ContainerWidgetProps } from "widgets/ContainerWidget/widget";
import type { WidgetProps } from "widgets/BaseWidget";
import { getContainerIdForCanvas } from "sagas/WidgetOperationUtils";
import scrollIntoView from "scroll-into-view-if-needed";
import validateColor from "validate-color";
export const snapToGrid = (
columnWidth: number,
@ -757,28 +758,36 @@ export function getLogToSentryFromResponse(response?: ApiResponse) {
return response && response?.responseMeta?.status >= 500;
}
const BLACKLIST_COLORS = ["#ffffff"];
const HEX_REGEX = /#[0-9a-fA-F]{6}/gi;
const RGB_REGEX = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/gi;
/**
* extract colors from string
*
* @param text
* @returns
* @param widgets
*/
export function extractColorsFromString(text: string) {
export function extractColorsFromString(widgets: CanvasWidgetsReduxState) {
const colors = new Set();
[...(text.match(RGB_REGEX) || []), ...(text.match(HEX_REGEX) || [])]
.filter((d) => BLACKLIST_COLORS.indexOf(d.toLowerCase()) === -1)
.forEach((color) => {
colors.add(color.toLowerCase());
Object.values(widgets).forEach((widget) => {
Object.values(widget).forEach((widgetProp) => {
if (isString(widgetProp) && validateColor(widgetProp)) {
colors.add(widgetProp);
}
});
});
return Array.from(colors) as Array<string>;
}
/**
* validate color string
*
* @returns {boolean} true if string is valid color or includes url
* @param color
*/
export function isValidColor(color: string) {
return color?.includes("url") || validateColor(color);
}
/*
* Function to merge property pane config of a widget
*

View File

@ -9589,6 +9589,7 @@ __metadata:
unescape-js: ^1.1.4
url-search-params-polyfill: ^8.0.0
uuid: ^9.0.0
validate-color: ^2.2.4
webfontloader: ^1.6.28
webpack-merge: ^5.8.0
webpack-retry-chunk-load-plugin: ^3.1.1
@ -28934,6 +28935,13 @@ __metadata:
languageName: node
linkType: hard
"validate-color@npm:^2.2.4":
version: 2.2.4
resolution: "validate-color@npm:2.2.4"
checksum: ac9109e6347797300fd0a0b08d623acaf3cc295cc2077b9041119582c632eb56dc78e94b8f7f929754508974a8a0b63d296995106a20b237b3be5e89ff2c80f1
languageName: node
linkType: hard
"validate-npm-package-license@npm:^3.0.1":
version: 3.0.4
resolution: "validate-npm-package-license@npm:3.0.4"