From edd9463cb6b65a5d8c61105bf7a7bccc0bae2155 Mon Sep 17 00:00:00 2001
From: skhamvon <126242816+skhamvon@users.noreply.github.com>
Date: Tue, 3 Oct 2023 09:40:21 +0200
Subject: [PATCH] feat(clipboard): add tooltip message for confirmation (#208)
* feat(clipboard): add tooltip message for confirmation
* feat(cdk): add new v-center strategies
* fix(clipboard): change display of host to fix cdk strategies issues
* fix(clipboard): missing await and duplicate style
* fix(clipboard): fix storybook
---
.../ocdk-surface-basic-position.ts | 2 +
.../ocdk-surface-tooltip-example.tsx | 6 +
.../ocdk-surface-tooltip-position.ts | 2 +
packages/cdk/dev/src/index.html | 12 +-
packages/cdk/dev/src/www.ts | 2 +-
.../surface/core/ocdk-surface-corner.ts | 2 +
.../core/ocdk-surface-normalized-corner.ts | 2 +
.../ocdk-surface-symmetry-strategy.ts | 6 +
.../symmetry/ocdk-surface-symmetry.cl-cr.ts | 134 ++++++++++++++++
.../symmetry/ocdk-surface-symmetry.cr-cl.ts | 134 ++++++++++++++++
packages/components/clipboard/package.json | 1 +
.../osds-clipboard/core/controller.spec.ts | 148 ++++++++++++++++++
.../osds-clipboard/core/controller.ts | 44 +++++-
.../osds-clipboard/interfaces/methods.ts | 10 ++
.../osds-clipboard/osds-clipboard.e2e.ts | 28 +++-
.../osds-clipboard/osds-clipboard.scss | 30 +++-
.../osds-clipboard/osds-clipboard.spec.ts | 59 +++++++
.../osds-clipboard/osds-clipboard.tsx | 68 ++++++--
.../components/osds-clipboard/public-api.ts | 1 +
packages/components/clipboard/src/index.html | 23 ++-
.../clipboard.web-components.stories.ts | 2 +
yarn.lock | 1 +
22 files changed, 690 insertions(+), 27 deletions(-)
create mode 100644 packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry.cl-cr.ts
create mode 100644 packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry.cr-cl.ts
create mode 100644 packages/components/clipboard/src/components/osds-clipboard/core/controller.spec.ts
create mode 100644 packages/components/clipboard/src/components/osds-clipboard/interfaces/methods.ts
diff --git a/packages/cdk/dev/src/components/surface/ocdk-surface-basic-example/ocdk-surface-basic-position.ts b/packages/cdk/dev/src/components/surface/ocdk-surface-basic-example/ocdk-surface-basic-position.ts
index 1375a65f39..6c965c0c2d 100644
--- a/packages/cdk/dev/src/components/surface/ocdk-surface-basic-example/ocdk-surface-basic-position.ts
+++ b/packages/cdk/dev/src/components/surface/ocdk-surface-basic-example/ocdk-surface-basic-position.ts
@@ -9,4 +9,6 @@ export const OcdkSurfaceBasicPositionList: string[] = [
'TOP_RIGHT BOTTOM_RIGHT',
'BOTTOM_CENTER TOP_CENTER',
'TOP_CENTER BOTTOM_CENTER',
+ 'CENTER_LEFT CENTER_RIGHT',
+ 'CENTER_RIGHT CENTER_LEFT',
];
diff --git a/packages/cdk/dev/src/components/surface/ocdk-surface-tooltip-example/ocdk-surface-tooltip-example.tsx b/packages/cdk/dev/src/components/surface/ocdk-surface-tooltip-example/ocdk-surface-tooltip-example.tsx
index 2bb49df68f..0fe87ca38b 100644
--- a/packages/cdk/dev/src/components/surface/ocdk-surface-tooltip-example/ocdk-surface-tooltip-example.tsx
+++ b/packages/cdk/dev/src/components/surface/ocdk-surface-tooltip-example/ocdk-surface-tooltip-example.tsx
@@ -115,6 +115,12 @@ export class OcdkSurfaceTooltipExample {
case OcdkSurfaceTooltipPosition.TOP_CENTER:
this.surface.setCornerPoints({ anchor: OcdkSurfaceCorner.TOP_CENTER, origin: OcdkSurfaceCorner.BOTTOM_CENTER });
break;
+ case OcdkSurfaceTooltipPosition.CENTER_LEFT:
+ this.surface.setCornerPoints({ anchor: OcdkSurfaceCorner.CENTER_LEFT, origin: OcdkSurfaceCorner.CENTER_RIGHT });
+ break;
+ case OcdkSurfaceTooltipPosition.CENTER_RIGHT:
+ this.surface.setCornerPoints({ anchor: OcdkSurfaceCorner.CENTER_RIGHT, origin: OcdkSurfaceCorner.CENTER_LEFT });
+ break;
}
}
diff --git a/packages/cdk/dev/src/components/surface/ocdk-surface-tooltip-example/ocdk-surface-tooltip-position.ts b/packages/cdk/dev/src/components/surface/ocdk-surface-tooltip-example/ocdk-surface-tooltip-position.ts
index a19992097b..2c0c4bd615 100644
--- a/packages/cdk/dev/src/components/surface/ocdk-surface-tooltip-example/ocdk-surface-tooltip-position.ts
+++ b/packages/cdk/dev/src/components/surface/ocdk-surface-tooltip-example/ocdk-surface-tooltip-position.ts
@@ -5,6 +5,8 @@ export enum OcdkSurfaceTooltipPosition {
RIGHT = 'RIGHT',
BOTTOM_CENTER = 'BOTTOM_CENTER',
TOP_CENTER = 'TOP_CENTER',
+ CENTER_LEFT = 'CENTER_LEFT',
+ CENTER_RIGHT = 'CENTER_RIGHT',
}
export type OcdkSurfaceTooltipPositionUnion = `${OcdkSurfaceTooltipPosition}`;
diff --git a/packages/cdk/dev/src/index.html b/packages/cdk/dev/src/index.html
index 8fe18ee120..6adf01c71f 100644
--- a/packages/cdk/dev/src/index.html
+++ b/packages/cdk/dev/src/index.html
@@ -256,7 +256,7 @@
tooltip example
- centered surface tooltip example
+ horizontal centered surface tooltip example
@@ -266,6 +266,16 @@ centered surface tooltip example
+ vertical centered surface tooltip example
+
+
+
+
+
+
+
+
+
diff --git a/packages/cdk/dev/src/www.ts b/packages/cdk/dev/src/www.ts
index cb785ba2f1..69ff442fe4 100644
--- a/packages/cdk/dev/src/www.ts
+++ b/packages/cdk/dev/src/www.ts
@@ -1,4 +1,4 @@
-import { Ods } from '@ovhcloud/ods-core';
+import { Ods } from '@ovhcloud/ods-common-core';
import { OcdkSurface, ocdkDefineCustomElements, OcdkSurfaceCorner } from '@ovhcloud/ods-cdk';
import {
OcdkSurfaceCustomStrategyExample
diff --git a/packages/cdk/src/components/surface/core/ocdk-surface-corner.ts b/packages/cdk/src/components/surface/core/ocdk-surface-corner.ts
index 0625f93575..0d189105bb 100644
--- a/packages/cdk/src/components/surface/core/ocdk-surface-corner.ts
+++ b/packages/cdk/src/components/surface/core/ocdk-surface-corner.ts
@@ -22,6 +22,8 @@ export enum OcdkSurfaceCorner {
BOTTOM_START = OcdkSurfaceCornerBit.BOTTOM | OcdkSurfaceCornerBit.FLIP_RTL, // tslint:disable-line:no-bitwise
/** 13 */
BOTTOM_END = OcdkSurfaceCornerBit.BOTTOM | OcdkSurfaceCornerBit.RIGHT | OcdkSurfaceCornerBit.FLIP_RTL, // tslint:disable-line:no-bitwise
+ CENTER_LEFT = 6,
+ CENTER_RIGHT = 7,
}
export const OcdkSurfaceCornerNameList = ocdkGetEnumNames(OcdkSurfaceCorner);
diff --git a/packages/cdk/src/components/surface/core/ocdk-surface-normalized-corner.ts b/packages/cdk/src/components/surface/core/ocdk-surface-normalized-corner.ts
index e9849ca5db..0798d706f4 100644
--- a/packages/cdk/src/components/surface/core/ocdk-surface-normalized-corner.ts
+++ b/packages/cdk/src/components/surface/core/ocdk-surface-normalized-corner.ts
@@ -13,4 +13,6 @@ export enum OcdkSurfaceNormalizedCorner {
BOTTOM_RIGHT = OcdkSurfaceCornerBit.BOTTOM | OcdkSurfaceCornerBit.RIGHT, // tslint:disable-line:no-bitwise
/** 3 */
BOTTOM_CENTER = 3,
+ CENTER_LEFT = 6,
+ CENTER_RIGHT = 7,
}
diff --git a/packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry-strategy.ts b/packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry-strategy.ts
index c27da1e30a..8ad789d592 100644
--- a/packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry-strategy.ts
+++ b/packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry-strategy.ts
@@ -11,6 +11,8 @@ import { ocdkSurfaceSymmetryTrBr } from './ocdk-surface-symmetry.tr-br';
import { ocdkSurfaceSymmetryTlTr } from './ocdk-surface-symmetry.tl-tr';
import { ocdkSurfaceSymmetryTrTl } from './ocdk-surface-symmetry.tr-tl';
import { ocdkSurfaceSymmetryTcBc } from './ocdk-surface-symmetry.tc-bc';
+import { ocdkSurfaceSymmetryClCr } from './ocdk-surface-symmetry.cl-cr';
+import { ocdkSurfaceSymmetryCrCl } from './ocdk-surface-symmetry.cr-cl';
/**
* global config to implement for the `symmetry` strategy
@@ -42,6 +44,10 @@ export class OcdkSurfaceSymmetryStrategy implements OcdkSurfaceStrategyDefiner {
+ const loggerSymmetry = new OcdkLogger('ocdkSurfaceSymmetryClCr');
+ const helpers = OcdkSurfaceSymmetryStrategyHelpers;
+
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.CENTER_LEFT,
+ origin: OcdkSurfaceNormalizedCorner.CENTER_RIGHT },
+ STRATEGIES: {
+ standard: {
+ inspectors: {
+ comfort: {
+ availableTop: (opt) => opt.measurements.viewportDistance.top - opt.config.anchorMargin.top - opt.config.MARGIN_TO_EDGE_COMFORT,
+ availableBottom: (opt) => opt.measurements.viewportDistance.bottom - opt.config.anchorMargin.bottom - opt.config.MARGIN_TO_EDGE_COMFORT,
+ availableLeft: (opt) => opt.measurements.viewportDistance.left - opt.config.anchorMargin.left - opt.config.MARGIN_TO_EDGE_COMFORT,
+ availableRight: (opt) => opt.measurements.viewportDistance.right - opt.config.anchorMargin.right - opt.config.MARGIN_TO_EDGE_COMFORT,
+ }
+ },
+ appliers: {
+ maxHeight: (opt) => opt.inspections.comfort.availableTop,
+ maxWidth: (opt) => opt.measurements.surfaceSize.width,
+ verticalOffset: (opt) => (-opt.measurements.surfaceSize.height / 2) + (opt.measurements.anchorSize.height / 2),
+ verticalAlignment: 'bottom',
+ horizontalOffset: (opt) => -opt.config.anchorMargin.left - opt.measurements.surfaceSize.width,
+ horizontalAlignment: 'left'
+ }
+ },
+ FALLBACK: {
+ inspectors: {
+ comfort: {
+ availableLeft: (opt) => opt.measurements.viewportSize.width - 2 * opt.config.MARGIN_TO_EDGE_COMFORT,
+ },
+ limit: {
+ availableLeft: (opt) => opt.measurements.viewportSize.width - 2 * opt.config.MARGIN_TO_EDGE_LIMIT,
+ }
+ },
+ appliers: {
+ maxHeight: (opt) => opt.measurements.surfaceSize.height,
+ maxWidth: (opt) => helpers.symmetryFallbackMaxWidth(opt, opt.inspections.comfort.availableLeft, opt.inspections.limit.availableLeft, false),
+ verticalOffset: (opt) => (-opt.measurements.surfaceSize.height / 2) + (opt.measurements.anchorSize.height / 2),
+ verticalAlignment: 'top',
+ horizontalOffset: (opt) => helpers.symmetryFallbackHorizontalOffset(opt, opt.inspections.comfort.availableLeft, opt.inspections.limit.availableLeft, true),
+ horizontalAlignment: 'right',
+ }
+ },
+ COMPUTE: (opt) => {
+ loggerSymmetry.log('[COMPUTE] position CENTER_LEFT CENTER_RIGhT');
+ // no enough available space on left, trigger a position change to right instead
+ if (opt.measurements.surfaceSize.width > opt.inspections.comfort.availableLeft) {
+ // already in a switch process and this new position isn't good enough, go to the fallback of the last strategy position
+ if (opt.switchFrom && isOcdkSurfaceStrategyComputeResultPosition(opt.switchFrom) && opt.switchFrom.position) {
+ loggerSymmetry.log('[COMPUTE] already switched off but no enough space: continue with the fallback of cr-cl', opt.switchFrom);
+ return opt.switchFrom.position.STRATEGIES.FALLBACK;
+ }
+ if ((opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2 > opt.inspections.comfort.availableTop) {
+ if (opt.measurements.surfaceSize.width > opt.inspections.comfort.availableRight && opt.inspections.comfort.availableBottom > (opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2) {
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.BOTTOM_LEFT,
+ origin: OcdkSurfaceNormalizedCorner.TOP_LEFT
+ }
+ };
+ }
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.TOP_RIGHT,
+ origin: OcdkSurfaceNormalizedCorner.TOP_LEFT
+ }
+ };
+ }
+ else if ((opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2 > opt.inspections.comfort.availableBottom) {
+ if (opt.measurements.surfaceSize.width > opt.inspections.comfort.availableRight) {
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.TOP_LEFT,
+ origin: OcdkSurfaceNormalizedCorner.BOTTOM_LEFT
+ }
+ };
+ }
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.BOTTOM_RIGHT,
+ origin: OcdkSurfaceNormalizedCorner.BOTTOM_LEFT
+ }
+ };
+ }
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.CENTER_RIGHT,
+ origin: OcdkSurfaceNormalizedCorner.CENTER_LEFT
+ }
+ };
+ }
+ else if ((opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2 > opt.inspections.comfort.availableTop) {
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.TOP_LEFT,
+ origin: OcdkSurfaceNormalizedCorner.TOP_RIGHT
+ }
+ };
+ }
+ else if ((opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2 > opt.inspections.comfort.availableBottom) {
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.BOTTOM_LEFT,
+ origin: OcdkSurfaceNormalizedCorner.BOTTOM_RIGHT
+ }
+ };
+ }
+ return; // no position switching: apply the current one
+ }
+
+ }
+ };
+}
diff --git a/packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry.cr-cl.ts b/packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry.cr-cl.ts
new file mode 100644
index 0000000000..3a508799af
--- /dev/null
+++ b/packages/cdk/src/components/surface/strategies/symmetry/ocdk-surface-symmetry.cr-cl.ts
@@ -0,0 +1,134 @@
+import { OcdkSurfaceSymmetryConfig } from './ocdk-surface-symmetry-strategy';
+import { OcdkSurfaceNormalizedCorner } from '../../core/ocdk-surface-normalized-corner';
+import { OcdkLogger } from '../../../../logger/ocdk-logger';
+import { OcdkSurfaceSymmetryStrategyHelpers } from './ocdk-surface-symmetry-strategy.helpers';
+import { isOcdkSurfaceStrategyComputeResultPosition } from '../../core/system/ocdk-surface-strategy-compute-result-position';
+import { OcdkSurfaceOnePositionStrategy } from '../../core/ocdk-surface-one-position-strategy';
+
+/**
+ * ```
+ * +-----------+
+ * +-----anchor-----+ | |
+ * | o o |
+ * +----------------+ | |
+ * +--surface--+
+ *
+ * o = normalized corner
+ * x = reference for the position offset (at top/left - for verticalAlignment/horizontalAlignment)
+ * ```
+ */
+export function ocdkSurfaceSymmetryCrCl(): OcdkSurfaceOnePositionStrategy {
+ const loggerSymmetry = new OcdkLogger('ocdkSurfaceSymmetryCrCl');
+ const helpers = OcdkSurfaceSymmetryStrategyHelpers;
+
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.CENTER_RIGHT,
+ origin: OcdkSurfaceNormalizedCorner.CENTER_LEFT },
+ STRATEGIES: {
+ standard: {
+ inspectors: {
+ comfort: {
+ availableTop: (opt) => opt.measurements.viewportDistance.top - opt.config.anchorMargin.top - opt.config.MARGIN_TO_EDGE_COMFORT,
+ availableBottom: (opt) => opt.measurements.viewportDistance.bottom - opt.config.anchorMargin.bottom - opt.config.MARGIN_TO_EDGE_COMFORT,
+ availableLeft: (opt) => opt.measurements.viewportDistance.left - opt.config.anchorMargin.left - opt.config.MARGIN_TO_EDGE_COMFORT,
+ availableRight: (opt) => opt.measurements.viewportDistance.right - opt.config.anchorMargin.right - opt.config.MARGIN_TO_EDGE_COMFORT,
+ }
+ },
+ appliers: {
+ maxHeight: (opt) => opt.inspections.comfort.availableTop,
+ maxWidth: (opt) => opt.measurements.surfaceSize.width,
+ verticalOffset: (opt) => (-opt.measurements.surfaceSize.height / 2) + (opt.measurements.anchorSize.height / 2),
+ verticalAlignment: 'top',
+ horizontalOffset: (opt) => -opt.config.anchorMargin.right - opt.measurements.surfaceSize.width,
+ horizontalAlignment: 'right'
+ }
+ },
+ FALLBACK: {
+ inspectors: {
+ comfort: {
+ availableRight: (opt) => opt.measurements.viewportSize.width - 2 * opt.config.MARGIN_TO_EDGE_COMFORT,
+ },
+ limit: {
+ availableRight: (opt) => opt.measurements.viewportSize.width - 2 * opt.config.MARGIN_TO_EDGE_LIMIT,
+ }
+ },
+ appliers: {
+ maxHeight: (opt) => helpers.symmetryFallbackMaxHeight(opt, opt.inspections.comfort.availableTop, opt.inspections.limit.availableTop, false),
+ maxWidth: (opt) => helpers.symmetryFallbackMaxWidth(opt, opt.inspections.comfort.availableRight, opt.inspections.limit.availableRight, false),
+ verticalOffset: (opt) => (-opt.measurements.surfaceSize.height / 2) + (opt.measurements.anchorSize.height / 2),
+ verticalAlignment: 'top',
+ horizontalOffset: (opt) => helpers.symmetryFallbackHorizontalOffset(opt, opt.inspections.comfort.availableRight, opt.inspections.limit.availableRight, true),
+ horizontalAlignment: 'left',
+ }
+ },
+ COMPUTE: (opt) => {
+ loggerSymmetry.log('[COMPUTE] position CENTER_RIGHT CENTER_LEFT');
+ // no enough available space on right, trigger a position change to left instead
+ if (opt.measurements.surfaceSize.width > opt.inspections.comfort.availableRight) {
+ // already in a switch process and this new position isn't good enough, go to the fallback of the last strategy position
+ if (opt.switchFrom && isOcdkSurfaceStrategyComputeResultPosition(opt.switchFrom) && opt.switchFrom.position) {
+ loggerSymmetry.log('[COMPUTE] already switched off but no enough space: continue with the fallback of cr-cl', opt.switchFrom);
+ return opt.switchFrom.position.STRATEGIES.FALLBACK;
+ }
+ if ((opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2 > opt.inspections.comfort.availableTop) {
+ if (opt.measurements.surfaceSize.width > opt.inspections.comfort.availableLeft && opt.inspections.comfort.availableBottom > (opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2) {
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.BOTTOM_RIGHT,
+ origin: OcdkSurfaceNormalizedCorner.TOP_RIGHT
+ }
+ };
+ }
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.TOP_LEFT,
+ origin: OcdkSurfaceNormalizedCorner.TOP_RIGHT
+ }
+ };
+ }
+ else if ((opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2 > opt.inspections.comfort.availableBottom) {
+ if (opt.measurements.surfaceSize.width > opt.inspections.comfort.availableLeft) {
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.TOP_RIGHT,
+ origin: OcdkSurfaceNormalizedCorner.BOTTOM_RIGHT
+ }
+ };
+ }
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.BOTTOM_LEFT,
+ origin: OcdkSurfaceNormalizedCorner.BOTTOM_RIGHT
+ }
+ };
+ }
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.CENTER_LEFT,
+ origin: OcdkSurfaceNormalizedCorner.CENTER_RIGHT
+ }
+ };
+ }
+ else if ((opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2 > opt.inspections.comfort.availableTop) {
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.TOP_RIGHT,
+ origin: OcdkSurfaceNormalizedCorner.TOP_LEFT
+ }
+ };
+ }
+ else if ((opt.measurements.surfaceSize.height - opt.measurements.anchorSize.height) / 2 > opt.inspections.comfort.availableBottom) {
+ return {
+ cornerPoints: {
+ anchor: OcdkSurfaceNormalizedCorner.BOTTOM_RIGHT,
+ origin: OcdkSurfaceNormalizedCorner.BOTTOM_LEFT
+ }
+ };
+ }
+ return; // no position switching: apply the current one
+ }
+
+ }
+ };
+}
diff --git a/packages/components/clipboard/package.json b/packages/components/clipboard/package.json
index 5a0aa92a84..301794a662 100644
--- a/packages/components/clipboard/package.json
+++ b/packages/components/clipboard/package.json
@@ -36,6 +36,7 @@
"test:e2e:ci:screenshot:update": "stencil test --config stencil.config.ts --e2e --ci --screenshot --update-screenshot --passWithNoTests"
},
"dependencies": {
+ "@ovhcloud/ods-cdk": "16.1.1",
"@ovhcloud/ods-common-core": "16.1.1",
"@ovhcloud/ods-common-stencil": "16.1.1",
"@ovhcloud/ods-common-theming": "16.1.1",
diff --git a/packages/components/clipboard/src/components/osds-clipboard/core/controller.spec.ts b/packages/components/clipboard/src/components/osds-clipboard/core/controller.spec.ts
new file mode 100644
index 0000000000..dbfff50874
--- /dev/null
+++ b/packages/components/clipboard/src/components/osds-clipboard/core/controller.spec.ts
@@ -0,0 +1,148 @@
+import type { OdsLoggerSpyReferences } from '@ovhcloud/ods-common-testing';
+import { OcdkSurface, OcdkSurfaceMock } from '@ovhcloud/ods-cdk';
+import { Ods, OdsLogger } from '@ovhcloud/ods-common-core';
+import { OdsClearLoggerSpy, OdsInitializeLoggerSpy } from '@ovhcloud/ods-common-testing';
+import { OdsClipboardController } from './controller';
+import { OsdsClipboard } from '../osds-clipboard';
+
+class OdsClipboardMock extends OsdsClipboard {
+ constructor(attribute: Partial) {
+ super();
+ Object.assign(this, attribute)
+ }
+}
+
+describe('spec:ods-clipboard-controller', () => {
+ let controller: OdsClipboardController;
+ let component: OsdsClipboard;
+ let loggerSpyReferences: OdsLoggerSpyReferences;
+
+ Ods.instance().logging(false);
+
+ function setup(attributes: Partial = {}) {
+ component = new OdsClipboardMock(attributes);
+ controller = new OdsClipboardController(component);
+ }
+
+ beforeEach(() => {
+ const loggerMocked = new OdsLogger('myLoggerMocked');
+ loggerSpyReferences = OdsInitializeLoggerSpy({
+ loggerMocked: loggerMocked as never,
+ spiedClass: OdsClipboardController
+ });
+ });
+
+ afterEach(() => {
+ OdsClearLoggerSpy(loggerSpyReferences);
+ jest.clearAllMocks();
+ });
+
+ it('should initialize', () => {
+ setup();
+ expect(controller).toBeTruthy();
+ });
+
+ describe('method: checkForClickOutside', () => {
+ it('should do nothing if there is no surface', async () => {
+ setup()
+
+ const event = new MouseEvent("click", {
+ bubbles: true,
+ cancelable: true,
+ composed: true
+ });
+
+ const target = document.createElement("OSDS-BUTTON");
+ Object.defineProperty(event, 'target', { value: target })
+
+
+ expect(() => { controller.checkForClickOutside(event) }).not.toThrow();
+ await expect(component.surface).toBeUndefined();
+ });
+
+ it('should do nothing if surface is not opened', async () => {
+ setup(component);
+
+ component.surface = new OcdkSurfaceMock() as unknown as OcdkSurface;
+ component.surface!.opened = false;
+
+ const event = new MouseEvent("click", {
+ bubbles: true,
+ cancelable: true,
+ composed: true
+ });
+
+ const target = document.createElement("OSDS-BUTTON");
+ Object.defineProperty(event, 'target', { value: target })
+
+ await controller.checkForClickOutside(event);
+ expect(component.surface.opened).toBe(false);
+ });
+
+ it('should do nothing if event target is in the component', async () => {
+ setup(component);
+ component.surface = new OcdkSurfaceMock() as unknown as OcdkSurface;
+ component.surface!.opened = true;
+
+ const event = new MouseEvent("click", {
+ bubbles: true,
+ cancelable: true,
+ composed: true
+ });
+
+ const target = document.createElement("OSDS-BUTTON");
+ Object.defineProperty(event, 'target', { value: target })
+
+ component.el.appendChild(target);
+
+ await controller.checkForClickOutside(event);
+ expect(component.surface.opened).toBeTruthy();
+ expect(component.surface.close).toHaveBeenCalledTimes(0);
+ });
+
+ it('should close the surface when click outside of the component', async () => {
+ setup(component);
+ component.surface = new OcdkSurfaceMock() as unknown as OcdkSurface;
+ component.surface!.opened = true;
+
+ const event = new MouseEvent("click", {
+ bubbles: true,
+ cancelable: true,
+ composed: true
+ });
+
+ const target = document.createElement("OSDS-BUTTON");
+ Object.defineProperty(event, 'target', { value: target })
+
+ await controller.checkForClickOutside(event);
+ expect(component.surface.close).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('method: closeSurface', () => {
+ it('should do nothing if there is no surface', async () => {
+ setup();
+
+ expect(() => { controller.closeSurface() }).not.toThrow();
+ expect(component.surface).toBeUndefined();
+ });
+ it('should do nothing if surface is closed', async () => {
+ setup(component);
+ component.surface = new OcdkSurfaceMock() as unknown as OcdkSurface;
+ component.surface!.opened = false;
+
+ expect(() => { controller.closeSurface() }).not.toThrow();
+ expect(component.surface.opened).toBe(false);
+ expect(component.surface.close).toHaveBeenCalledTimes(0);
+
+ });
+ it('should close the surface', async () => {
+ setup(component);
+ component.surface = new OcdkSurfaceMock() as unknown as OcdkSurface;
+ component.surface!.opened = true;
+
+ await controller.closeSurface();
+ expect(component.surface.close).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/packages/components/clipboard/src/components/osds-clipboard/core/controller.ts b/packages/components/clipboard/src/components/osds-clipboard/core/controller.ts
index 8d1e7017a7..98296bc472 100644
--- a/packages/components/clipboard/src/components/osds-clipboard/core/controller.ts
+++ b/packages/components/clipboard/src/components/osds-clipboard/core/controller.ts
@@ -1,8 +1,50 @@
+import type { OsdsClipboard } from '../osds-clipboard';
import { writeOnClipboard } from '@ovhcloud/ods-common-core';
class OdsClipboardController {
+ private component: OsdsClipboard;
+
+ constructor(component: OsdsClipboard) {
+ this.component = component
+ }
+
async handlerClick(value: string): Promise {
- await writeOnClipboard(value);
+ const successMessage = this.component.el.querySelector('[slot=success-message]')?.innerHTML;
+ const errorMessage = this.component.el.querySelector('[slot=error-message]')?.innerHTML;
+
+ try {
+ await writeOnClipboard(value);
+ this.component.surfaceMessage = successMessage;
+ if (this.component.surface && this.component.surfaceMessage !== "") {
+ this.component.surface.opened = !this.component.surface.opened;
+ }
+ } catch (error) {
+ this.component.surfaceMessage = errorMessage;
+ if (this.component.surface && this.component.surfaceMessage !== "") {
+ this.component.surface.opened = !this.component.surface.opened;
+ }
+ throw error;
+ }
+ }
+
+ syncReferences(): void {
+ if (this.component.surface && this.component.anchor) {
+ this.component.surface.setAnchorElement(this.component.anchor);
+ }
+ }
+
+ closeSurface(): void {
+ if (this.component.surface && this.component.surface.opened) {
+ this.component.surface.close();
+ }
+ }
+
+ checkForClickOutside(event: MouseEvent): void {
+ if (this.component.el.contains(event.target as Node) || this.component.surface === undefined || !this.component.surface.opened) {
+ return;
+ }
+
+ this.closeSurface();
}
}
diff --git a/packages/components/clipboard/src/components/osds-clipboard/interfaces/methods.ts b/packages/components/clipboard/src/components/osds-clipboard/interfaces/methods.ts
new file mode 100644
index 0000000000..f13804a1eb
--- /dev/null
+++ b/packages/components/clipboard/src/components/osds-clipboard/interfaces/methods.ts
@@ -0,0 +1,10 @@
+interface OdsClipboardMethod {
+ /**
+ * Close the surface
+ */
+ closeSurface(): Promise;
+}
+
+export {
+ OdsClipboardMethod,
+};
diff --git a/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.e2e.ts b/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.e2e.ts
index 32bf9c094a..f58910887e 100644
--- a/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.e2e.ts
+++ b/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.e2e.ts
@@ -9,6 +9,7 @@ describe('e2e:osds-clipboard', () => {
let page: E2EPage;
let el: E2EElement;
let input: E2EElement;
+ let clipboardSurface: E2EElement;
async function mockClipboard(page: E2EPage): Promise {
await page.evaluate(() => {
@@ -22,11 +23,11 @@ describe('e2e:osds-clipboard', () => {
});
}
- async function setup({ attributes = {} }: { attributes?: Partial } = {}) {
+ async function setup({ attributes = {}, html = "" }: { attributes?: Partial, html?: string } = {}) {
const stringAttributes = odsComponentAttributes2StringAttributes({ ...baseAttribute, ...attributes }, DEFAULT_ATTRIBUTE);
page = await newE2EPage();
- await page.setContent(``);
+ await page.setContent(`${html}`);
const origin = await page.evaluate(() => window.origin);
const browserContext = page.browserContext();
@@ -37,6 +38,8 @@ describe('e2e:osds-clipboard', () => {
el = await page.find('osds-clipboard');
input = await page.find('osds-clipboard >>> osds-input');
+ clipboardSurface = await page.find('osds-clipboard >>> ocdk-surface');
+
await page.waitForChanges();
}
@@ -90,4 +93,25 @@ describe('e2e:osds-clipboard', () => {
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe('');
});
+
+ it('should show the surface when clicked on', async () => {
+ const value = 'text to copy';
+ const messages = `CopiedError`
+
+ await setup({ attributes: { value }, html: messages });
+
+ await input.click();
+ expect(clipboardSurface).toHaveClass('ocdk-surface--open')
+ });
+
+ it('should hide the surface when a click happened outside of the surface', async () => {
+ const value = 'text to copy';
+ const messages = `CopiedError`
+
+ await setup({ attributes: { value }, html: messages });
+
+ await input.click();
+ await el.click();
+ expect(clipboardSurface).not.toHaveClass('ocdk-surface--open')
+ });
});
diff --git a/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.scss b/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.scss
index 703afea902..6e97770fa7 100644
--- a/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.scss
+++ b/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.scss
@@ -2,11 +2,23 @@
@import '~@ovhcloud/ods-common-theming/ods-theme';
:host {
- osds-input {
- cursor: pointer;
- --ods-input-cursor: pointer;
- --ods-input-icon-cursor: pointer;
- }
+ display: block;
+
+ /* overlay important properties */
+ position: relative; /* must be here to make the positioning working well */
+ text-align: initial; /* must be here to make the positioning working well for RTL alignment */
+ padding: 0; /* must be here to make the computing works. If you need padding, apply it on trigger or surface */
+ /* end overlay important properties */
+
+ osds-input {
+ cursor: pointer;
+ --ods-input-cursor: pointer;
+ --ods-input-icon-cursor: pointer;
+ }
+}
+
+:host([inline]) {
+ display: inline-flex;
}
:host([disabled]) {
@@ -17,3 +29,11 @@
--ods-input-icon-cursor: not-allowed;
}
}
+
+ocdk-surface {
+ padding: 16px;
+ width: max-content;
+ max-width: 300px;
+ background: var(--white) 0% 0% no-repeat padding-box;
+ box-shadow: 0px 0px 6px #00000026;
+}
diff --git a/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.spec.ts b/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.spec.ts
index 82d50d8e11..d94be76d4a 100644
--- a/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.spec.ts
+++ b/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.spec.ts
@@ -1,7 +1,9 @@
+jest.mock('@ovhcloud/ods-cdk'); // keep jest.mock before any import
jest.mock('./core/controller'); // keep jest.mock before any
import type { SpecPage } from '@stencil/core/testing';
import type { OdsClipboardAttribute } from './interfaces/attributes';
+import { ocdkIsSurface } from '@ovhcloud/ods-cdk';
import { newSpecPage } from '@stencil/core/testing';
import { odsComponentAttributes2StringAttributes, odsStringAttributes2Str, odsUnitTestAttribute } from '@ovhcloud/ods-common-testing';
import { DEFAULT_ATTRIBUTE } from './constants/default-attributes';
@@ -20,6 +22,10 @@ describe('spec:osds-clipboard', () => {
jest.clearAllMocks();
});
+ function mockSurfaceElements() {
+ (ocdkIsSurface as unknown as jest.Mock).mockImplementation(() => true);
+ }
+
async function setup({ attributes = {} }: { attributes?: Partial } = {}) {
const stringAttributes = odsComponentAttributes2StringAttributes({ ...baseAttribute, ...attributes }, DEFAULT_ATTRIBUTE);
@@ -40,6 +46,59 @@ describe('spec:osds-clipboard', () => {
expect(instance).toBeTruthy();
});
+ describe('cdk not initialized', () => {
+ it('should not have yet the ref to surface', async () => {
+ (ocdkIsSurface as unknown as jest.Mock).mockImplementation(() => false);
+ await setup();
+ expect(instance.surface).toBe(undefined);
+ })
+ });
+
+ describe('cdk initialized', () => {
+ it('should have ref to anchor', async () => {
+ await setup();
+ expect(instance.anchor).toBeTruthy();
+ })
+
+ it('should have ref to surface', async () => {
+ mockSurfaceElements();
+ await setup();
+ expect(instance.surface).toBeTruthy();
+ })
+
+ it('should call syncReferences of controller for anchor and surface', async () => {
+ mockSurfaceElements();
+ await setup();
+ expect(controller.syncReferences).toHaveBeenCalledTimes(2);
+ expect(controller.syncReferences).toHaveBeenCalledWith();
+ });
+ })
+
+ describe('controller', () => {
+ it('should call handlerClick of controller', async () => {
+ const string = "test"
+ await setup( { attributes: { value: string } });
+ instance.handlerClick();
+ expect(controller.handlerClick).toHaveBeenCalledTimes(1);
+ expect(controller.handlerClick).toHaveBeenCalledWith(string);
+ });
+
+ it('should call checkForClickOutside of controller', async () => {
+ const event = new Event('click');
+ await setup();
+ instance.checkForClickOutside(event);
+ expect(controller.checkForClickOutside).toHaveBeenCalledTimes(1);
+ expect(controller.checkForClickOutside).toHaveBeenCalledWith(event);
+ })
+
+ it('should call closeSurface of controller', async () => {
+ await setup();
+ await instance.closeSurface();
+ expect(controller.closeSurface).toHaveBeenCalledTimes(1);
+ expect(controller.closeSurface).toHaveBeenCalledWith();
+ })
+ });
+
describe('attributes', () => {
const config = {
instance: () => instance,
diff --git a/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.tsx b/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.tsx
index eaa8e8e079..fd5ea4556f 100644
--- a/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.tsx
+++ b/packages/components/clipboard/src/components/osds-clipboard/osds-clipboard.tsx
@@ -1,20 +1,29 @@
import type { EventEmitter } from '@stencil/core';
import type { OdsClipboardAttribute } from './interfaces/attributes';
import type { OdsClipboardEvent } from './interfaces/events';
-import { Component, Host, h, Prop, Event } from '@stencil/core';
+import type { OdsClipboardMethod } from './interfaces/methods';
+import { Component, Host, h, Prop, Event, Listen, Element, Method, State } from '@stencil/core';
+import { ocdkDefineCustomElements, ocdkIsSurface, OcdkSurface, OcdkSurfaceCorner } from '@ovhcloud/ods-cdk';
import { ODS_THEME_COLOR_INTENT } from '@ovhcloud/ods-common-theming';
import { ODS_ICON_NAME } from '@ovhcloud/ods-component-icon';
import { ODS_INPUT_TYPE } from '@ovhcloud/ods-component-input';
import { DEFAULT_ATTRIBUTE } from './constants/default-attributes';
import { OdsClipboardController } from './core/controller';
+import { HTMLStencilElement } from '@stencil/core/internal';
+
+ocdkDefineCustomElements();
@Component({
tag: 'osds-clipboard',
styleUrl: 'osds-clipboard.scss',
shadow: true
})
-export class OsdsClipboard implements OdsClipboardAttribute, OdsClipboardEvent {
- controller: OdsClipboardController = new OdsClipboardController();
+export class OsdsClipboard implements OdsClipboardAttribute, OdsClipboardEvent, OdsClipboardMethod {
+ controller: OdsClipboardController = new OdsClipboardController(this);
+ anchor!: HTMLDivElement;
+ surface: OcdkSurface | undefined = undefined;
+
+ @Element() el!: HTMLStencilElement;
/** @see OdsClipboardAttributes.inline */
@Prop({ reflect: true }) public inline?: boolean = DEFAULT_ATTRIBUTE.inline;
@@ -25,9 +34,21 @@ export class OsdsClipboard implements OdsClipboardAttribute, OdsClipboardEvent {
/** @see OdsClipboardAttributes.disabled */
@Prop({ reflect: true }) public disabled?: boolean = DEFAULT_ATTRIBUTE.disabled;
+ @State() surfaceMessage: string | undefined = "";
+
+ @Method()
+ async closeSurface() {
+ this.controller.closeSurface();
+ }
+
/** @see OdsClipboardEvents.odsClipboardCopied */
@Event() odsClipboardCopied!: EventEmitter;
+ @Listen('click', { target: 'window' })
+ checkForClickOutside(event: any) {
+ this.controller.checkForClickOutside(event);
+ }
+
handlerClick(): void {
if (this.disabled) {
return;
@@ -45,20 +66,39 @@ export class OsdsClipboard implements OdsClipboardAttribute, OdsClipboardEvent {
}
}
+ syncReferences() {
+ this.controller.syncReferences()
+ }
+
render() {
return (
- this.handlerClick() }
- onKeyDown={ (event: KeyboardEvent) => this.handlerKeyDown(event) }
- >
-
+ {
+ this.anchor = el as HTMLDivElement;
+ this.syncReferences()
+ }}>
+ this.handlerClick()}
+ onKeyDown={(event: KeyboardEvent) => this.handlerKeyDown(event)}
+ >
+
+
+ {
+ if (ocdkIsSurface(el)) {
+ this.surface = el as OcdkSurface;
+ this.syncReferences()
+ }
+ }}/>
);
}
diff --git a/packages/components/clipboard/src/components/osds-clipboard/public-api.ts b/packages/components/clipboard/src/components/osds-clipboard/public-api.ts
index c00cbbd47c..0eab6600cc 100644
--- a/packages/components/clipboard/src/components/osds-clipboard/public-api.ts
+++ b/packages/components/clipboard/src/components/osds-clipboard/public-api.ts
@@ -1,3 +1,4 @@
export type { OdsClipboardAttribute } from './interfaces/attributes';
export type { OdsClipboardEvent } from './interfaces/events';
+export type { OdsClipboardMethod } from './interfaces/methods';
export { OsdsClipboard } from './osds-clipboard';
diff --git a/packages/components/clipboard/src/index.html b/packages/components/clipboard/src/index.html
index a7411b527b..0d810dbbec 100644
--- a/packages/components/clipboard/src/index.html
+++ b/packages/components/clipboard/src/index.html
@@ -15,11 +15,28 @@
-Clipboard
-Clipboard
+
+
+ Clipboard
+ Copied
+ Error
+
Clipboard inline
-Clipboard
+
+ Clipboard
+ Copied2
+ Error2
+
+
+
+
+ Clipboard
+ Copied3
+ Error3
+
+
+
Clipboard disabled
Clipboard
diff --git a/packages/storybook/stories/components/clipboard/clipboard.web-components.stories.ts b/packages/storybook/stories/components/clipboard/clipboard.web-components.stories.ts
index 698363c1bf..87ba6a0d37 100644
--- a/packages/storybook/stories/components/clipboard/clipboard.web-components.stories.ts
+++ b/packages/storybook/stories/components/clipboard/clipboard.web-components.stories.ts
@@ -40,6 +40,8 @@ const TemplateDefault = (args:any) => {
return html`
e.stopPropagation()}>
Clipboard
+ Success
+ Error
`;
}
diff --git a/yarn.lock b/yarn.lock
index 52687074a8..783408a5a8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4536,6 +4536,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@ovhcloud/ods-component-clipboard@workspace:packages/components/clipboard"
dependencies:
+ "@ovhcloud/ods-cdk": 16.1.1
"@ovhcloud/ods-common-core": 16.1.1
"@ovhcloud/ods-common-stencil": 16.1.1
"@ovhcloud/ods-common-testing": 16.1.1