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